Skip to main content

sova_core/response/
mod.rs

1mod file;
2mod typed;
3
4pub use typed::{referer_or, Html, Json, NoContent, Redirect, Text};
5
6use crate::error::IntoResponse;
7use bytes::Bytes;
8use futures_util::TryStreamExt;
9use http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
10use http_body::Frame;
11use http_body_util::combinators::BoxBody;
12use http_body_util::{BodyExt, Full, StreamBody};
13use serde::Serialize;
14use std::convert::Infallible;
15use std::mem;
16use std::path::Path;
17
18pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
19/// Boxed HTTP body used for both request streams and response streams.
20pub type HttpBody = BoxBody<Bytes, BoxError>;
21/// Alias for [`HttpBody`] (historical name for response streaming).
22pub type ResponseBody = HttpBody;
23
24/// Express-style HTTP response.
25pub struct Response {
26    pub(crate) status: StatusCode,
27    pub(crate) headers: HeaderMap,
28    pub(crate) body: Body,
29}
30
31impl std::fmt::Debug for Response {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("Response")
34            .field("status", &self.status)
35            .finish_non_exhaustive()
36    }
37}
38
39pub enum Body {
40    Bytes(Bytes),
41    Stream(ResponseBody),
42}
43
44impl From<Bytes> for Body {
45    fn from(b: Bytes) -> Self {
46        Body::Bytes(b)
47    }
48}
49
50impl From<Vec<u8>> for Body {
51    fn from(b: Vec<u8>) -> Self {
52        Body::Bytes(Bytes::from(b))
53    }
54}
55
56impl From<&'static [u8]> for Body {
57    fn from(b: &'static [u8]) -> Self {
58        Body::Bytes(Bytes::from_static(b))
59    }
60}
61
62impl From<String> for Body {
63    fn from(s: String) -> Self {
64        Body::Bytes(Bytes::from(s))
65    }
66}
67
68impl Body {
69    /// Buffer the entire body into memory (pays for streams intentionally).
70    pub async fn collect(self) -> Result<Bytes, BoxError> {
71        match self {
72            Body::Bytes(b) => Ok(b),
73            Body::Stream(stream) => {
74                let collected = BodyExt::collect(stream).await?;
75                Ok(collected.to_bytes())
76            }
77        }
78    }
79}
80
81impl Default for Response {
82    fn default() -> Self {
83        Self::empty()
84    }
85}
86
87impl Response {
88    pub fn empty() -> Self {
89        Self {
90            status: StatusCode::OK,
91            headers: HeaderMap::new(),
92            body: Body::Bytes(Bytes::new()),
93        }
94    }
95
96    pub fn text(body: impl Into<String>) -> Self {
97        let mut res = Self::empty();
98        res.set_text(body.into());
99        res
100    }
101
102    pub fn html(body: impl Into<String>) -> Self {
103        let mut res = Self::empty();
104        res.headers.insert(
105            http::header::CONTENT_TYPE,
106            HeaderValue::from_static("text/html; charset=utf-8"),
107        );
108        res.body = Body::Bytes(Bytes::from(body.into()));
109        res
110    }
111
112    pub fn json<T: Serialize>(value: &T) -> Self {
113        match serde_json::to_vec(value) {
114            Ok(bytes) => {
115                let mut res = Self::empty();
116                res.headers.insert(
117                    http::header::CONTENT_TYPE,
118                    HeaderValue::from_static("application/json"),
119                );
120                res.body = Body::Bytes(Bytes::from(bytes));
121                res
122            }
123            Err(err) => Self::text(format!("JSON encode error: {err}")).status(500),
124        }
125    }
126
127    pub fn redirect(location: impl AsRef<str>) -> Self {
128        Redirect::to(location.as_ref()).into_response()
129    }
130
131    /// Buffered body with an explicit MIME type.
132    pub fn bytes(data: impl Into<Bytes>, mime: &str) -> Self {
133        let mut res = Self::empty();
134        if let Ok(v) = HeaderValue::from_str(mime) {
135            res.headers.insert(http::header::CONTENT_TYPE, v);
136        }
137        let data = data.into();
138        if let Ok(v) = HeaderValue::from_str(&data.len().to_string()) {
139            res.headers.insert(http::header::CONTENT_LENGTH, v);
140        }
141        res.body = Body::Bytes(data);
142        res
143    }
144
145    /// Set `Content-Disposition: attachment` for downloads.
146    pub fn attachment(mut self, filename: &str) -> Self {
147        let safe = filename.replace(['"', '\r', '\n', '\\'], "_");
148        let value = format!("attachment; filename=\"{safe}\"");
149        if let Ok(v) = HeaderValue::from_str(&value) {
150            self.headers.insert(http::header::CONTENT_DISPOSITION, v);
151        }
152        self
153    }
154
155    /// Server-Sent Events stream (`text/event-stream`).
156    ///
157    /// Each item becomes one `data:` event (multi-line values split across `data:` lines).
158    pub fn sse<S, E>(stream: S) -> Self
159    where
160        S: futures_util::Stream<Item = Result<String, E>> + Send + Sync + 'static,
161        E: Into<BoxError> + Send + 'static,
162    {
163        use futures_util::StreamExt;
164        let mapped = stream.map(|item| {
165            item.map(|s| {
166                let mut out = String::new();
167                for line in s.split('\n') {
168                    out.push_str("data: ");
169                    out.push_str(line);
170                    out.push('\n');
171                }
172                out.push('\n');
173                Bytes::from(out)
174            })
175            .map_err(Into::into)
176        });
177        let mapped = mapped.map_ok(Frame::data).map_err(|e: BoxError| e);
178        let mut res = Self::stream(BodyExt::boxed(StreamBody::new(mapped)));
179        res.headers.insert(
180            http::header::CONTENT_TYPE,
181            HeaderValue::from_static("text/event-stream"),
182        );
183        res.headers.insert(
184            http::header::CACHE_CONTROL,
185            HeaderValue::from_static("no-cache"),
186        );
187        res
188    }
189
190    pub fn stream(body: ResponseBody) -> Self {
191        let mut res = Self::empty();
192        res.body = Body::Stream(body);
193        res
194    }
195
196    pub fn from_reader_stream<S>(stream: S) -> Self
197    where
198        S: futures_util::Stream<Item = Result<Bytes, std::io::Error>> + Send + Sync + 'static,
199    {
200        let mapped = stream.map_ok(Frame::data).map_err(|e| -> BoxError { Box::new(e) });
201        Self::stream(BodyExt::boxed(StreamBody::new(mapped)))
202    }
203
204    /// Chainable status: `Response::json(&x).status(201)`.
205    pub fn status(mut self, code: u16) -> Self {
206        self.status = StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
207        self
208    }
209
210    pub fn header(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Self {
211        if let (Ok(name), Ok(value)) = (
212            HeaderName::from_bytes(name.as_ref().as_bytes()),
213            HeaderValue::from_str(value.as_ref()),
214        ) {
215            self.headers.insert(name, value);
216        }
217        self
218    }
219
220    pub fn status_code(&self) -> StatusCode {
221        self.status
222    }
223
224    pub fn headers(&self) -> &HeaderMap {
225        &self.headers
226    }
227
228    pub fn headers_mut(&mut self) -> &mut HeaderMap {
229        &mut self.headers
230    }
231
232    pub fn take_body(&mut self) -> Body {
233        mem::replace(&mut self.body, Body::Bytes(Bytes::new()))
234    }
235
236    pub fn set_body(&mut self, body: impl Into<Body>) {
237        self.body = body.into();
238    }
239
240    /// Body bytes when the response is buffered; `None` for streams.
241    pub fn body_bytes(&self) -> Option<&[u8]> {
242        match &self.body {
243            Body::Bytes(b) => Some(b.as_ref()),
244            Body::Stream(_) => None,
245        }
246    }
247
248    /// Stream a local file (path-traversal safe relative to its parent directory).
249    pub async fn file(path: impl AsRef<Path>) -> Self {
250        file::serve_path(path.as_ref()).await
251    }
252
253    /// Stream a file under `dir` / `relative` (path-traversal safe).
254    pub async fn file_in(dir: impl AsRef<Path>, relative: impl AsRef<Path>) -> Self {
255        file::serve_in(dir.as_ref(), relative.as_ref()).await
256    }
257
258    /// [`Self::file`] plus `Content-Disposition: attachment`.
259    pub async fn download(path: impl AsRef<Path>) -> Self {
260        let path = path.as_ref();
261        let name = path
262            .file_name()
263            .and_then(|s| s.to_str())
264            .unwrap_or("download");
265        Self::file(path).await.attachment(name)
266    }
267
268    /// [`Self::file_in`] plus `Content-Disposition: attachment`.
269    pub async fn download_in(dir: impl AsRef<Path>, relative: impl AsRef<Path>) -> Self {
270        let relative = relative.as_ref();
271        let name = relative
272            .file_name()
273            .and_then(|s| s.to_str())
274            .unwrap_or("download");
275        Self::file_in(dir, relative).await.attachment(name)
276    }
277
278    pub(crate) fn clear_body(&mut self) {
279        self.body = Body::Bytes(Bytes::new());
280    }
281
282    fn set_text(&mut self, body: String) {
283        self.headers.insert(
284            http::header::CONTENT_TYPE,
285            HeaderValue::from_static("text/plain; charset=utf-8"),
286        );
287        self.body = Body::Bytes(Bytes::from(body));
288    }
289
290    pub(crate) fn into_http_body(self) -> ResponseBody {
291        match self.body {
292            Body::Bytes(b) => Full::new(b)
293                .map_err(|_: Infallible| unreachable!())
294                .boxed(),
295            Body::Stream(b) => b,
296        }
297    }
298}
299