Skip to main content

tower_http/services/fs/
serve_file.rs

1//! Service that serves a file.
2
3use super::ServeDir;
4use http::{HeaderValue, Request};
5use mime::Mime;
6use std::{
7    path::Path,
8    task::{Context, Poll},
9};
10use tower_service::Service;
11
12/// Service that serves a file.
13#[derive(Clone, Debug)]
14pub struct ServeFile(ServeDir);
15
16// Note that this is just a special case of ServeDir
17impl ServeFile {
18    /// Create a new [`ServeFile`].
19    ///
20    /// The `Content-Type` will be guessed from the file extension.
21    pub fn new<P: AsRef<Path>>(path: P) -> Self {
22        let guess = mime_guess::from_path(path.as_ref());
23        let mime = guess
24            .first_raw()
25            .map(HeaderValue::from_static)
26            .unwrap_or_else(|| {
27                HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
28            });
29
30        Self(ServeDir::new_single_file(path, mime))
31    }
32
33    /// Create a new [`ServeFile`] with a specific mime type.
34    ///
35    /// # Panics
36    ///
37    /// Will panic if the mime type isn't a valid [header value].
38    ///
39    /// [header value]: https://docs.rs/http/latest/http/header/struct.HeaderValue.html
40    pub fn new_with_mime<P: AsRef<Path>>(path: P, mime: &Mime) -> Self {
41        let mime = HeaderValue::from_str(mime.as_ref()).expect("mime isn't a valid header value");
42        Self(ServeDir::new_single_file(path, mime))
43    }
44
45    /// Informs the service that it should also look for a precompressed gzip
46    /// version of the file.
47    ///
48    /// If the client has an `Accept-Encoding` header that allows the gzip encoding,
49    /// the file `foo.txt.gz` will be served instead of `foo.txt`.
50    /// If the precompressed file is not available, or the client doesn't support it,
51    /// the uncompressed version will be served instead.
52    /// Both the precompressed version and the uncompressed version are expected
53    /// to be present in the same directory. Different precompressed
54    /// variants can be combined.
55    pub fn precompressed_gzip(self) -> Self {
56        Self(self.0.precompressed_gzip())
57    }
58
59    /// Informs the service that it should also look for a precompressed brotli
60    /// version of the file.
61    ///
62    /// If the client has an `Accept-Encoding` header that allows the brotli encoding,
63    /// the file `foo.txt.br` will be served instead of `foo.txt`.
64    /// If the precompressed file is not available, or the client doesn't support it,
65    /// the uncompressed version will be served instead.
66    /// Both the precompressed version and the uncompressed version are expected
67    /// to be present in the same directory. Different precompressed
68    /// variants can be combined.
69    pub fn precompressed_br(self) -> Self {
70        Self(self.0.precompressed_br())
71    }
72
73    /// Informs the service that it should also look for a precompressed deflate
74    /// version of the file.
75    ///
76    /// If the client has an `Accept-Encoding` header that allows the deflate encoding,
77    /// the file `foo.txt.zz` will be served instead of `foo.txt`.
78    /// If the precompressed file is not available, or the client doesn't support it,
79    /// the uncompressed version will be served instead.
80    /// Both the precompressed version and the uncompressed version are expected
81    /// to be present in the same directory. Different precompressed
82    /// variants can be combined.
83    pub fn precompressed_deflate(self) -> Self {
84        Self(self.0.precompressed_deflate())
85    }
86
87    /// Informs the service that it should also look for a precompressed zstd
88    /// version of the file.
89    ///
90    /// If the client has an `Accept-Encoding` header that allows the zstd encoding,
91    /// the file `foo.txt.zst` will be served instead of `foo.txt`.
92    /// If the precompressed file is not available, or the client doesn't support it,
93    /// the uncompressed version will be served instead.
94    /// Both the precompressed version and the uncompressed version are expected
95    /// to be present in the same directory. Different precompressed
96    /// variants can be combined.
97    pub fn precompressed_zstd(self) -> Self {
98        Self(self.0.precompressed_zstd())
99    }
100
101    /// Set a specific read buffer chunk size.
102    ///
103    /// The default capacity is 64kb.
104    pub fn with_buf_chunk_size(self, chunk_size: usize) -> Self {
105        Self(self.0.with_buf_chunk_size(chunk_size))
106    }
107
108    /// Configure whether syntactically valid multi-range requests should be ignored.
109    ///
110    /// See [`ServeDir::ignore_multi_range_requests`] for details.
111    pub fn ignore_multi_range_requests(self, ignore: bool) -> Self {
112        Self(self.0.ignore_multi_range_requests(ignore))
113    }
114
115    /// Call the service and get a future that contains any `std::io::Error` that might have
116    /// happened.
117    ///
118    /// See [`ServeDir::try_call`] for more details.
119    pub fn try_call<ReqBody>(
120        &mut self,
121        req: Request<ReqBody>,
122    ) -> super::serve_dir::future::ResponseFuture<ReqBody>
123    where
124        ReqBody: Send + 'static,
125    {
126        self.0.try_call(req)
127    }
128}
129
130impl<ReqBody> Service<Request<ReqBody>> for ServeFile
131where
132    ReqBody: Send + 'static,
133{
134    type Error = <ServeDir as Service<Request<ReqBody>>>::Error;
135    type Response = <ServeDir as Service<Request<ReqBody>>>::Response;
136    type Future = <ServeDir as Service<Request<ReqBody>>>::Future;
137
138    #[inline]
139    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
140        Poll::Ready(Ok(()))
141    }
142
143    #[inline]
144    fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
145        self.0.call(req)
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use crate::services::ServeFile;
152    use crate::test_helpers::Body;
153    use brotli::BrotliDecompress;
154    use flate2::bufread::DeflateDecoder;
155    use flate2::bufread::GzDecoder;
156    use http::header;
157    use http::Method;
158    use http::{Request, StatusCode};
159    use http_body_util::BodyExt;
160    use mime::Mime;
161    use std::io::Read;
162    use std::str::FromStr;
163    use tower::ServiceExt;
164
165    /// Expected prefix of the decompressed content in precompressed test files.
166    const EXPECTED_CONTENT_PREFIX: &str = "Test file";
167
168    /// Directory containing test fixture files.
169    const TEST_FILES_DIR: &str = "../test-files";
170    /// Path to the repository README, used as a large test fixture.
171    const README_PATH: &str = "../README.md";
172
173    #[tokio::test]
174    async fn basic() {
175        let svc = ServeFile::new(README_PATH);
176
177        let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
178
179        assert_eq!(res.headers()["content-type"], "text/markdown");
180
181        let body = res.into_body().collect().await.unwrap().to_bytes();
182        let body = String::from_utf8(body.to_vec()).unwrap();
183
184        assert!(body.starts_with("# Tower HTTP"));
185    }
186
187    #[tokio::test]
188    async fn basic_with_mime() {
189        let svc = ServeFile::new_with_mime(README_PATH, &Mime::from_str("image/jpg").unwrap());
190
191        let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
192
193        assert_eq!(res.headers()["content-type"], "image/jpg");
194
195        let body = res.into_body().collect().await.unwrap().to_bytes();
196        let body = String::from_utf8(body.to_vec()).unwrap();
197
198        assert!(body.starts_with("# Tower HTTP"));
199    }
200
201    #[tokio::test]
202    async fn multipart_range_can_be_ignored() {
203        let svc = ServeFile::new(README_PATH).ignore_multi_range_requests(true);
204        let request = Request::builder()
205            .header(header::RANGE, "bytes=0-0,2-2")
206            .body(Body::empty())
207            .unwrap();
208        let res = svc.oneshot(request).await.unwrap();
209
210        assert_eq!(res.status(), StatusCode::OK);
211        assert!(res.headers().get(header::CONTENT_RANGE).is_none());
212
213        let body = res.into_body().collect().await.unwrap().to_bytes();
214        assert_eq!(body.as_ref(), std::fs::read(README_PATH).unwrap());
215    }
216
217    #[tokio::test]
218    async fn head_request() {
219        let svc = ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt"));
220
221        let mut request = Request::new(Body::empty());
222        *request.method_mut() = Method::HEAD;
223        let res = svc.oneshot(request).await.unwrap();
224
225        assert_eq!(res.headers()["content-type"], "text/plain");
226        assert_eq!(res.headers()["content-length"], "10");
227
228        assert!(res.into_body().frame().await.is_none());
229    }
230
231    #[tokio::test]
232    async fn precompressed_head_request() {
233        let svc =
234            ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_gzip();
235
236        let request = Request::builder()
237            .header("Accept-Encoding", "gzip")
238            .method(Method::HEAD)
239            .body(Body::empty())
240            .unwrap();
241        let res = svc.oneshot(request).await.unwrap();
242
243        assert_eq!(res.headers()["content-type"], "text/plain");
244        assert_eq!(res.headers()["content-encoding"], "gzip");
245        assert_eq!(res.headers()["content-length"], "30");
246
247        assert!(res.into_body().frame().await.is_none());
248    }
249
250    #[tokio::test]
251    async fn precompressed_gzip() {
252        let svc =
253            ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_gzip();
254
255        let request = Request::builder()
256            .header("Accept-Encoding", "gzip")
257            .body(Body::empty())
258            .unwrap();
259        let res = svc.oneshot(request).await.unwrap();
260
261        assert_eq!(res.headers()["content-type"], "text/plain");
262        assert_eq!(res.headers()["content-encoding"], "gzip");
263
264        let body = res.into_body().collect().await.unwrap().to_bytes();
265        let mut decoder = GzDecoder::new(&body[..]);
266        let mut decompressed = String::new();
267        decoder.read_to_string(&mut decompressed).unwrap();
268        assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
269    }
270
271    #[tokio::test]
272    async fn unsupported_precompression_alogrithm_fallbacks_to_uncompressed() {
273        let svc =
274            ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_gzip();
275
276        let request = Request::builder()
277            .header("Accept-Encoding", "br")
278            .body(Body::empty())
279            .unwrap();
280        let res = svc.oneshot(request).await.unwrap();
281
282        assert_eq!(res.headers()["content-type"], "text/plain");
283        assert!(res.headers().get("content-encoding").is_none());
284
285        let body = res.into_body().collect().await.unwrap().to_bytes();
286        let body = String::from_utf8(body.to_vec()).unwrap();
287        assert!(body.starts_with(EXPECTED_CONTENT_PREFIX));
288    }
289
290    #[tokio::test]
291    async fn missing_precompressed_variant_fallbacks_to_uncompressed() {
292        let svc = ServeFile::new(format!("{TEST_FILES_DIR}/missing_precompressed.txt"))
293            .precompressed_gzip();
294
295        let request = Request::builder()
296            .header("Accept-Encoding", "gzip")
297            .body(Body::empty())
298            .unwrap();
299        let res = svc.oneshot(request).await.unwrap();
300
301        assert_eq!(res.headers()["content-type"], "text/plain");
302        // Uncompressed file is served because compressed version is missing
303        assert!(res.headers().get("content-encoding").is_none());
304
305        let body = res.into_body().collect().await.unwrap().to_bytes();
306        let body = String::from_utf8(body.to_vec()).unwrap();
307        assert!(body.starts_with(EXPECTED_CONTENT_PREFIX));
308    }
309
310    #[tokio::test]
311    async fn missing_precompressed_variant_fallbacks_to_uncompressed_head_request() {
312        let svc = ServeFile::new(format!("{TEST_FILES_DIR}/missing_precompressed.txt"))
313            .precompressed_gzip();
314
315        let request = Request::builder()
316            .header("Accept-Encoding", "gzip")
317            .method(Method::HEAD)
318            .body(Body::empty())
319            .unwrap();
320        let res = svc.oneshot(request).await.unwrap();
321
322        assert_eq!(res.headers()["content-type"], "text/plain");
323        assert_eq!(res.headers()["content-length"], "10");
324        // Uncompressed file is served because compressed version is missing
325        assert!(res.headers().get("content-encoding").is_none());
326
327        assert!(res.into_body().frame().await.is_none());
328    }
329
330    #[tokio::test]
331    async fn only_precompressed_variant_existing() {
332        let svc = ServeFile::new(format!("{TEST_FILES_DIR}/only_gzipped.txt")).precompressed_gzip();
333
334        let request = Request::builder().body(Body::empty()).unwrap();
335        let res = svc.clone().oneshot(request).await.unwrap();
336
337        assert_eq!(res.status(), StatusCode::NOT_FOUND);
338
339        // Should reply with gzipped file if client supports it
340        let request = Request::builder()
341            .header("Accept-Encoding", "gzip")
342            .body(Body::empty())
343            .unwrap();
344        let res = svc.oneshot(request).await.unwrap();
345
346        assert_eq!(res.headers()["content-type"], "text/plain");
347        assert_eq!(res.headers()["content-encoding"], "gzip");
348
349        let body = res.into_body().collect().await.unwrap().to_bytes();
350        let mut decoder = GzDecoder::new(&body[..]);
351        let mut decompressed = String::new();
352        decoder.read_to_string(&mut decompressed).unwrap();
353        assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
354    }
355
356    #[tokio::test]
357    async fn precompressed_br() {
358        let svc = ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_br();
359
360        let request = Request::builder()
361            .header("Accept-Encoding", "gzip,br")
362            .body(Body::empty())
363            .unwrap();
364        let res = svc.oneshot(request).await.unwrap();
365
366        assert_eq!(res.headers()["content-type"], "text/plain");
367        assert_eq!(res.headers()["content-encoding"], "br");
368
369        let body = res.into_body().collect().await.unwrap().to_bytes();
370        let mut decompressed = Vec::new();
371        BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
372        let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
373        assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
374    }
375
376    #[tokio::test]
377    async fn precompressed_deflate() {
378        let svc =
379            ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_deflate();
380        let request = Request::builder()
381            .header("Accept-Encoding", "deflate,br")
382            .body(Body::empty())
383            .unwrap();
384        let res = svc.oneshot(request).await.unwrap();
385
386        assert_eq!(res.headers()["content-type"], "text/plain");
387        assert_eq!(res.headers()["content-encoding"], "deflate");
388
389        let body = res.into_body().collect().await.unwrap().to_bytes();
390        let mut decoder = DeflateDecoder::new(&body[..]);
391        let mut decompressed = String::new();
392        decoder.read_to_string(&mut decompressed).unwrap();
393        assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
394    }
395
396    #[tokio::test]
397    async fn precompressed_zstd() {
398        let svc =
399            ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_zstd();
400        let request = Request::builder()
401            .header("Accept-Encoding", "zstd,br")
402            .body(Body::empty())
403            .unwrap();
404        let res = svc.oneshot(request).await.unwrap();
405
406        assert_eq!(res.headers()["content-type"], "text/plain");
407        assert_eq!(res.headers()["content-encoding"], "zstd");
408
409        let body = res.into_body().collect().await.unwrap().to_bytes();
410        let decompressed = zstd::stream::decode_all(&body[..]).unwrap();
411        let decompressed = String::from_utf8(decompressed).unwrap();
412        assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
413    }
414
415    #[tokio::test]
416    async fn multi_precompressed() {
417        let svc = ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt"))
418            .precompressed_gzip()
419            .precompressed_br();
420
421        let request = Request::builder()
422            .header("Accept-Encoding", "gzip")
423            .body(Body::empty())
424            .unwrap();
425        let res = svc.clone().oneshot(request).await.unwrap();
426
427        assert_eq!(res.headers()["content-type"], "text/plain");
428        assert_eq!(res.headers()["content-encoding"], "gzip");
429
430        let body = res.into_body().collect().await.unwrap().to_bytes();
431        let mut decoder = GzDecoder::new(&body[..]);
432        let mut decompressed = String::new();
433        decoder.read_to_string(&mut decompressed).unwrap();
434        assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
435
436        let request = Request::builder()
437            .header("Accept-Encoding", "br")
438            .body(Body::empty())
439            .unwrap();
440        let res = svc.clone().oneshot(request).await.unwrap();
441
442        assert_eq!(res.headers()["content-type"], "text/plain");
443        assert_eq!(res.headers()["content-encoding"], "br");
444
445        let body = res.into_body().collect().await.unwrap().to_bytes();
446        let mut decompressed = Vec::new();
447        BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
448        let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
449        assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
450    }
451
452    #[tokio::test]
453    async fn with_custom_chunk_size() {
454        let svc = ServeFile::new(README_PATH).with_buf_chunk_size(1024 * 32);
455
456        let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
457
458        assert_eq!(res.headers()["content-type"], "text/markdown");
459
460        let body = res.into_body().collect().await.unwrap().to_bytes();
461        let body = String::from_utf8(body.to_vec()).unwrap();
462
463        assert!(body.starts_with("# Tower HTTP"));
464    }
465
466    #[tokio::test]
467    async fn fallbacks_to_different_precompressed_variant_if_not_found() {
468        let svc = ServeFile::new(format!("{TEST_FILES_DIR}/precompressed_br.txt"))
469            .precompressed_gzip()
470            .precompressed_deflate()
471            .precompressed_br();
472
473        let request = Request::builder()
474            .header("Accept-Encoding", "gzip,deflate,br")
475            .body(Body::empty())
476            .unwrap();
477        let res = svc.oneshot(request).await.unwrap();
478
479        assert_eq!(res.headers()["content-type"], "text/plain");
480        assert_eq!(res.headers()["content-encoding"], "br");
481
482        let body = res.into_body().collect().await.unwrap().to_bytes();
483        let mut decompressed = Vec::new();
484        BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
485        let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
486        assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
487    }
488
489    #[tokio::test]
490    async fn fallbacks_to_different_precompressed_variant_if_not_found_head_request() {
491        let svc = ServeFile::new(format!("{TEST_FILES_DIR}/precompressed_br.txt"))
492            .precompressed_gzip()
493            .precompressed_deflate()
494            .precompressed_br();
495
496        let request = Request::builder()
497            .header("Accept-Encoding", "gzip,deflate,br")
498            .method(Method::HEAD)
499            .body(Body::empty())
500            .unwrap();
501        let res = svc.oneshot(request).await.unwrap();
502
503        assert_eq!(res.headers()["content-type"], "text/plain");
504        assert_eq!(res.headers()["content-length"], "15");
505        assert_eq!(res.headers()["content-encoding"], "br");
506
507        assert!(res.into_body().frame().await.is_none());
508    }
509
510    #[tokio::test]
511    async fn returns_404_if_file_doesnt_exist() {
512        let svc = ServeFile::new("../this-doesnt-exist.md");
513
514        let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
515
516        assert_eq!(res.status(), StatusCode::NOT_FOUND);
517        assert!(res.headers().get(header::CONTENT_TYPE).is_none());
518    }
519
520    #[tokio::test]
521    async fn returns_404_if_file_doesnt_exist_when_precompression_is_used() {
522        let svc = ServeFile::new("../this-doesnt-exist.md").precompressed_deflate();
523
524        let request = Request::builder()
525            .header("Accept-Encoding", "deflate")
526            .body(Body::empty())
527            .unwrap();
528        let res = svc.oneshot(request).await.unwrap();
529
530        assert_eq!(res.status(), StatusCode::NOT_FOUND);
531        assert!(res.headers().get(header::CONTENT_TYPE).is_none());
532    }
533
534    #[tokio::test]
535    async fn last_modified() {
536        let svc = ServeFile::new(README_PATH);
537
538        let req = Request::builder().body(Body::empty()).unwrap();
539        let res = svc.oneshot(req).await.unwrap();
540
541        assert_eq!(res.status(), StatusCode::OK);
542
543        let last_modified = res
544            .headers()
545            .get(header::LAST_MODIFIED)
546            .expect("Missing last modified header!");
547
548        // -- If-Modified-Since
549
550        let svc = ServeFile::new(README_PATH);
551        let req = Request::builder()
552            .header(header::IF_MODIFIED_SINCE, last_modified)
553            .body(Body::empty())
554            .unwrap();
555
556        let res = svc.oneshot(req).await.unwrap();
557        assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
558        assert!(res.into_body().frame().await.is_none());
559
560        let svc = ServeFile::new(README_PATH);
561        let req = Request::builder()
562            .header(header::IF_MODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
563            .body(Body::empty())
564            .unwrap();
565
566        let res = svc.oneshot(req).await.unwrap();
567        assert_eq!(res.status(), StatusCode::OK);
568        let readme_bytes = include_bytes!("../../../../README.md");
569        let body = res.into_body().collect().await.unwrap().to_bytes();
570        assert_eq!(body.as_ref(), readme_bytes);
571
572        // -- If-Unmodified-Since
573
574        let svc = ServeFile::new(README_PATH);
575        let req = Request::builder()
576            .header(header::IF_UNMODIFIED_SINCE, last_modified)
577            .body(Body::empty())
578            .unwrap();
579
580        let res = svc.oneshot(req).await.unwrap();
581        assert_eq!(res.status(), StatusCode::OK);
582        let body = res.into_body().collect().await.unwrap().to_bytes();
583        assert_eq!(body.as_ref(), readme_bytes);
584
585        let svc = ServeFile::new(README_PATH);
586        let req = Request::builder()
587            .header(header::IF_UNMODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
588            .body(Body::empty())
589            .unwrap();
590
591        let res = svc.oneshot(req).await.unwrap();
592        assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
593        assert!(res.into_body().frame().await.is_none());
594    }
595}