wayver's git archive


a simple self-hosted git server
git clone https://git.wayver.dev/bile

src/http/response.rs@3aec1a1bb245ce255f59532eba2bdfe1d2879510

raw
Date Commit Message Author Files + -
2026-02-20 13:35 custom path extractor for better error handling wayverd 14 108 49
...

1use std::sync::Arc;
2
3use axum::{
4    http::{HeaderValue, StatusCode, header},
5    response::{IntoResponse, Response},
6};
7
8use crate::{config::Config, http::BileState};
9
10pub(crate) type Result<T = Response, E = crate::error::Error> = std::result::Result<T, E>;
11
12pub(crate) struct Css<T>(pub T);
13
14impl<T: IntoResponse> IntoResponse for Css<T> {
15    fn into_response(self) -> Response {
16        (
17            [
18                (
19                    header::CONTENT_TYPE,
20                    HeaderValue::from_static(mime::TEXT_CSS_UTF_8.as_ref()),
21                ),
22                (
23                    header::CACHE_CONTROL,
24                    HeaderValue::from_static("max-age=31536000, immutable"),
25                ),
26            ],
27            self.0,
28        )
29            .into_response()
30    }
31}
32
33pub(crate) struct Ico<T>(pub T);
34
35impl<T: IntoResponse> IntoResponse for Ico<T> {
36    fn into_response(self) -> Response {
37        (
38            [
39                (
40                    header::CONTENT_TYPE,
41                    HeaderValue::from_static("image/x-icon"),
42                ),
43                (
44                    header::CACHE_CONTROL,
45                    HeaderValue::from_static("max-age=31536000, immutable"),
46                ),
47            ],
48            self.0,
49        )
50            .into_response()
51    }
52}
53
54pub(crate) struct Json<T>(pub T);
55
56impl<T: IntoResponse> IntoResponse for Json<T> {
57    fn into_response(self) -> Response {
58        (
59            [
60                (
61                    header::CONTENT_TYPE,
62                    HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
63                ),
64                (
65                    header::CACHE_CONTROL,
66                    HeaderValue::from_static("max-age=31536000, immutable"),
67                ),
68            ],
69            self.0,
70        )
71            .into_response()
72    }
73}
74
75pub(crate) struct Png<T>(pub T);
76
77impl<T: IntoResponse> IntoResponse for Png<T> {
78    fn into_response(self) -> Response {
79        (
80            [
81                (
82                    header::CONTENT_TYPE,
83                    HeaderValue::from_static(mime::IMAGE_PNG.as_ref()),
84                ),
85                (
86                    header::CACHE_CONTROL,
87                    HeaderValue::from_static("max-age=31536000, immutable"),
88                ),
89            ],
90            self.0,
91        )
92            .into_response()
93    }
94}
95
96pub(crate) struct Text<T>(pub T);
97
98impl<T: IntoResponse> IntoResponse for Text<T> {
99    fn into_response(self) -> Response {
100        (
101            [
102                (
103                    header::CONTENT_TYPE,
104                    HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
105                ),
106                (
107                    header::CACHE_CONTROL,
108                    HeaderValue::from_static("max-age=300, private"),
109                ),
110            ],
111            self.0,
112        )
113            .into_response()
114    }
115}
116
117pub(crate) struct Html<T: askama::Template>(pub T);
118
119impl<T: askama::Template> IntoResponse for Html<T> {
120    fn into_response(self) -> Response {
121        match self.0.render() {
122            Ok(rendered) => (
123                [
124                    (
125                        header::CONTENT_TYPE,
126                        HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()),
127                    ),
128                    (
129                        header::CACHE_CONTROL,
130                        HeaderValue::from_static("max-age=300, private"),
131                    ),
132                ],
133                rendered,
134            )
135                .into_response(),
136            Err(err) => {
137                tracing::error!(err=?err, "failed to render html response");
138
139                (
140                    StatusCode::INTERNAL_SERVER_ERROR,
141                    [(
142                        header::CONTENT_TYPE,
143                        HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()),
144                    )],
145                    "a serious error has occured",
146                )
147                    .into_response()
148            }
149        }
150    }
151}
152
153pub(crate) struct Xml<T: askama::Template>(pub T);
154
155impl<T: askama::Template> IntoResponse for Xml<T> {
156    fn into_response(self) -> Response {
157        match self.0.render() {
158            Ok(rendered) => (
159                [
160                    (
161                        header::CONTENT_TYPE,
162                        HeaderValue::from_static(mime::TEXT_XML.as_ref()),
163                    ),
164                    (
165                        header::CACHE_CONTROL,
166                        HeaderValue::from_static("max-age=300, private"),
167                    ),
168                ],
169                rendered,
170            )
171                .into_response(),
172            Err(err) => {
173                tracing::error!(err=?err, "failed to render xml response");
174
175                (
176                    StatusCode::INTERNAL_SERVER_ERROR,
177                    [(
178                        header::CONTENT_TYPE,
179                        HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()),
180                    )],
181                    "a serious error has occured",
182                )
183                    .into_response()
184            }
185        }
186    }
187}
188
189#[must_use = "needs to be returned from a handler or otherwise turned into a Response to be useful"]
190#[derive(Debug, Clone)]
191pub(crate) struct Redirect {
192    status_code: StatusCode,
193    location: HeaderValue,
194}
195
196impl Redirect {
197    pub(crate) const PERMANENT_ROOT: Self = Self {
198        status_code: StatusCode::PERMANENT_REDIRECT,
199        location: HeaderValue::from_static("/"),
200    };
201    pub(crate) const TEMPORARY_ROOT: Self = Self {
202        status_code: StatusCode::TEMPORARY_REDIRECT,
203        location: HeaderValue::from_static("/"),
204    };
205
206    #[tracing::instrument(skip_all)]
207    pub(crate) fn to(uri: &str) -> Option<Self> {
208        Self::with_status_code(StatusCode::SEE_OTHER, uri)
209    }
210
211    #[tracing::instrument(skip_all)]
212    pub(crate) fn temporary(uri: &str) -> Option<Self> {
213        Self::with_status_code(StatusCode::TEMPORARY_REDIRECT, uri)
214    }
215
216    #[tracing::instrument(skip_all)]
217    pub(crate) fn permanent(uri: &str) -> Option<Self> {
218        Self::with_status_code(StatusCode::PERMANENT_REDIRECT, uri)
219    }
220
221    #[tracing::instrument(skip_all)]
222    fn with_status_code(status_code: StatusCode, uri: &str) -> Option<Self> {
223        assert!(
224            status_code.is_redirection(),
225            "not a redirection status code"
226        );
227
228        let location = match HeaderValue::try_from(uri) {
229            Ok(location) => location,
230            Err(err) => {
231                tracing::error!(err=?err, "failed to convert uri to header");
232
233                return None;
234            }
235        };
236
237        Some(Self {
238            status_code,
239            location,
240        })
241    }
242}
243
244impl IntoResponse for Redirect {
245    fn into_response(self) -> Response {
246        (self.status_code, [(header::LOCATION, self.location)]).into_response()
247    }
248}
249
250#[derive(askama::Template)]
251#[template(path = "error.html")]
252pub(crate) struct ErrorPage {
253    config: Arc<Config>,
254    status: StatusCode,
255}
256
257impl ErrorPage {
258    pub(crate) fn with_status(self, status: StatusCode) -> Self {
259        Self {
260            config: self.config,
261            status,
262        }
263    }
264}
265
266impl From<BileState> for ErrorPage {
267    fn from(value: BileState) -> Self {
268        Self {
269            config: value.config,
270            status: StatusCode::INTERNAL_SERVER_ERROR,
271        }
272    }
273}
274
275impl<'s> From<&'s BileState> for ErrorPage {
276    fn from(value: &'s BileState) -> Self {
277        Self {
278            config: value.config.clone(),
279            status: StatusCode::INTERNAL_SERVER_ERROR,
280        }
281    }
282}
283
284impl IntoResponse for ErrorPage {
285    fn into_response(self) -> Response {
286        (self.status, Html(self)).into_response()
287    }
288}
289