Skip to main content

actix_http/encoding/
decoder.rs

1//! Stream decoders.
2
3use std::{
4    future::Future,
5    io::{self, Write as _},
6    pin::Pin,
7    task::{Context, Poll},
8};
9
10use bytes::Bytes;
11#[cfg(feature = "compress-gzip")]
12use flate2::write::{GzDecoder, ZlibDecoder};
13use futures_core::{ready, Stream};
14use tokio::task::{spawn_blocking, JoinHandle};
15#[cfg(feature = "compress-zstd")]
16use zstd::stream::write::Decoder as ZstdDecoder;
17
18use crate::{
19    encoding::Writer,
20    error::PayloadError,
21    header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
22};
23
24const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
25
26pin_project_lite::pin_project! {
27    /// Payload stream that decompresses chunks using a content encoding.
28    ///
29    /// Yields decoded [`Bytes`] as they become available. Errors from the underlying stream are
30    /// passed through; decompression errors are returned as [`PayloadError::Io`].
31    ///
32    /// Exceeding the configured decoded byte limit returns [`PayloadError::Overflow`].
33    ///
34    /// See the [module-level docs](crate::encoding) for supported encodings and runtime requirements.
35    pub struct Decoder<S> {
36        decoder: Option<ContentDecoder>,
37        #[pin]
38        stream: S,
39        eof: bool,
40        fut: Option<JoinHandle<io::Result<(Option<Bytes>, ContentDecoder)>>>,
41    }
42}
43
44impl<S> Decoder<S>
45where
46    S: Stream<Item = Result<Bytes, PayloadError>>,
47{
48    /// Constructs a decoder with the given content encoding.
49    ///
50    /// If `encoding` is [`ContentEncoding::Identity`] or its codec feature is disabled, the stream
51    /// is passed through unchanged.
52    #[inline]
53    pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
54        #[cfg(feature = "compress-brotli")]
55        const DEFAULT_DECODER_CAPACITY: usize = 8 * 1024; // 8 KiB
56
57        let decoder = match encoding {
58            #[cfg(feature = "compress-brotli")]
59            ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new(
60                brotli::DecompressorWriter::new(Writer::new(), DEFAULT_DECODER_CAPACITY),
61            ))),
62
63            #[cfg(feature = "compress-gzip")]
64            ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(ZlibDecoder::new(
65                Writer::new(),
66            )))),
67
68            #[cfg(feature = "compress-gzip")]
69            ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(GzDecoder::new(
70                Writer::new(),
71            )))),
72
73            #[cfg(feature = "compress-zstd")]
74            ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new(
75                ZstdDecoder::new(Writer::new()).expect(
76                    "Failed to create zstd decoder. This is a bug. \
77                         Please report it to the actix-web repository.",
78                ),
79            ))),
80
81            _ => None,
82        };
83
84        Decoder {
85            decoder,
86            stream,
87            fut: None,
88            eof: false,
89        }
90    }
91
92    /// Set the maximum total number of decoded bytes.
93    ///
94    /// Decoding stops with [`PayloadError::Overflow`] if the limit is exceeded.
95    ///
96    /// Limit must be set before the stream is first polled.
97    ///
98    /// By default, decoded output is unlimited.
99    pub fn limit(mut self, limit: usize) -> Self {
100        if let Some(ref mut decoder) = self.decoder {
101            decoder.set_limit(limit);
102        }
103
104        self
105    }
106
107    /// Constructs a decoder using the `Content-Encoding` header.
108    ///
109    /// If the header is missing, invalid, or unrecognized, the stream is passed through unchanged.
110    /// A recognized encoding whose codec feature is disabled also passes through unchanged.
111    ///
112    /// Only a single encoding is supported. Comma-separated encoding lists are treated as
113    /// unrecognized values. If there are multiple `Content-Encoding` header values, only the first
114    /// is used.
115    #[inline]
116    pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> {
117        // check content-encoding
118        let encoding = headers
119            .get(&CONTENT_ENCODING)
120            .and_then(|val| val.to_str().ok())
121            .and_then(|x| x.parse().ok())
122            .unwrap_or(ContentEncoding::Identity);
123
124        Self::new(stream, encoding)
125    }
126}
127
128impl<S> Stream for Decoder<S>
129where
130    S: Stream<Item = Result<Bytes, PayloadError>>,
131{
132    type Item = Result<Bytes, PayloadError>;
133
134    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
135        let mut this = self.project();
136
137        loop {
138            // Receive chunk and decoder state back from thread-offloaded decode task.
139            if let Some(ref mut fut) = this.fut {
140                let result = ready!(Pin::new(fut).poll(cx))
141                    .map_err(|_| {
142                        PayloadError::Io(io::Error::other(
143                            "Blocking task was cancelled unexpectedly",
144                        ))
145                    })
146                    .and_then(|result| result.map_err(from_decoder_error));
147
148                // Offload task completed; throw away join handle.
149                this.fut.take();
150
151                let (chunk, decoder) = match result {
152                    Ok(result) => result,
153                    Err(err) => {
154                        *this.eof = true;
155                        return Poll::Ready(Some(Err(err)));
156                    }
157                };
158
159                *this.decoder = Some(decoder);
160
161                if let Some(chunk) = chunk {
162                    return Poll::Ready(Some(Ok(chunk)));
163                }
164            }
165
166            if *this.eof {
167                return Poll::Ready(None);
168            }
169
170            match ready!(this.stream.as_mut().poll_next(cx)) {
171                Some(Err(err)) => return Poll::Ready(Some(Err(err))),
172
173                Some(Ok(chunk)) => {
174                    if let Some(mut decoder) = this.decoder.take() {
175                        if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE {
176                            let chunk = match decoder.feed_data(chunk) {
177                                Ok(chunk) => chunk,
178                                Err(err) => {
179                                    *this.eof = true;
180                                    return Poll::Ready(Some(Err(from_decoder_error(err))));
181                                }
182                            };
183
184                            *this.decoder = Some(decoder);
185
186                            if let Some(chunk) = chunk {
187                                return Poll::Ready(Some(Ok(chunk)));
188                            }
189                        } else {
190                            *this.fut = Some(spawn_blocking(move || {
191                                let chunk = decoder.feed_data(chunk)?;
192                                Ok((chunk, decoder))
193                            }));
194                        }
195
196                        continue;
197                    } else {
198                        return Poll::Ready(Some(Ok(chunk)));
199                    }
200                }
201
202                None => {
203                    *this.eof = true;
204
205                    return if let Some(mut decoder) = this.decoder.take() {
206                        match decoder.feed_eof() {
207                            Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
208                            Ok(None) => Poll::Ready(None),
209                            Err(err) => Poll::Ready(Some(Err(from_decoder_error(err)))),
210                        }
211                    } else {
212                        Poll::Ready(None)
213                    };
214                }
215            }
216        }
217    }
218}
219
220fn from_decoder_error(err: io::Error) -> PayloadError {
221    match err.kind() {
222        io::ErrorKind::FileTooLarge => PayloadError::Overflow,
223        _ => err.into(),
224    }
225}
226
227enum ContentDecoder {
228    #[cfg(feature = "compress-gzip")]
229    Deflate(Box<ZlibDecoder<Writer>>),
230
231    #[cfg(feature = "compress-gzip")]
232    Gzip(Box<GzDecoder<Writer>>),
233
234    #[cfg(feature = "compress-brotli")]
235    Brotli(Box<brotli::DecompressorWriter<Writer>>),
236
237    // We need 'static here because:
238    // - ZstdDecoder needs a lifetime argument;
239    // - we use `spawn_blocking` in `Decoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
240    #[cfg(feature = "compress-zstd")]
241    Zstd(Box<ZstdDecoder<'static, Writer>>),
242}
243
244impl ContentDecoder {
245    fn set_limit(&mut self, limit: usize) {
246        match self {
247            #[cfg(feature = "compress-brotli")]
248            ContentDecoder::Brotli(ref mut decoder) => decoder.get_mut().set_limit(limit),
249
250            #[cfg(feature = "compress-gzip")]
251            ContentDecoder::Gzip(ref mut decoder) => decoder.get_mut().set_limit(limit),
252
253            #[cfg(feature = "compress-gzip")]
254            ContentDecoder::Deflate(ref mut decoder) => decoder.get_mut().set_limit(limit),
255
256            #[cfg(feature = "compress-zstd")]
257            ContentDecoder::Zstd(ref mut decoder) => decoder.get_mut().set_limit(limit),
258        }
259    }
260
261    fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
262        match self {
263            #[cfg(feature = "compress-brotli")]
264            ContentDecoder::Brotli(ref mut decoder) => match decoder.flush() {
265                Ok(()) => {
266                    let b = decoder.get_mut().take();
267
268                    if !b.is_empty() {
269                        Ok(Some(b))
270                    } else {
271                        Ok(None)
272                    }
273                }
274                Err(err) => Err(err),
275            },
276
277            #[cfg(feature = "compress-gzip")]
278            ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() {
279                Ok(_) => {
280                    let b = decoder.get_mut().take();
281
282                    if !b.is_empty() {
283                        Ok(Some(b))
284                    } else {
285                        Ok(None)
286                    }
287                }
288                Err(err) => Err(err),
289            },
290
291            #[cfg(feature = "compress-gzip")]
292            ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
293                Ok(_) => {
294                    let b = decoder.get_mut().take();
295                    if !b.is_empty() {
296                        Ok(Some(b))
297                    } else {
298                        Ok(None)
299                    }
300                }
301                Err(err) => Err(err),
302            },
303
304            #[cfg(feature = "compress-zstd")]
305            ContentDecoder::Zstd(ref mut decoder) => match decoder.flush() {
306                Ok(_) => {
307                    let b = decoder.get_mut().take();
308                    if !b.is_empty() {
309                        Ok(Some(b))
310                    } else {
311                        Ok(None)
312                    }
313                }
314                Err(err) => Err(err),
315            },
316        }
317    }
318
319    fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
320        match self {
321            #[cfg(feature = "compress-brotli")]
322            ContentDecoder::Brotli(ref mut decoder) => match decoder.write_all(&data) {
323                Ok(_) => {
324                    decoder.flush()?;
325                    let b = decoder.get_mut().take();
326
327                    if !b.is_empty() {
328                        Ok(Some(b))
329                    } else {
330                        Ok(None)
331                    }
332                }
333                Err(err) => Err(err),
334            },
335
336            #[cfg(feature = "compress-gzip")]
337            ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) {
338                Ok(_) => {
339                    decoder.flush()?;
340                    let b = decoder.get_mut().take();
341
342                    if !b.is_empty() {
343                        Ok(Some(b))
344                    } else {
345                        Ok(None)
346                    }
347                }
348                Err(err) => Err(err),
349            },
350
351            #[cfg(feature = "compress-gzip")]
352            ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
353                Ok(_) => {
354                    decoder.flush()?;
355
356                    let b = decoder.get_mut().take();
357                    if !b.is_empty() {
358                        Ok(Some(b))
359                    } else {
360                        Ok(None)
361                    }
362                }
363                Err(err) => Err(err),
364            },
365
366            #[cfg(feature = "compress-zstd")]
367            ContentDecoder::Zstd(ref mut decoder) => match decoder.write_all(&data) {
368                Ok(_) => {
369                    decoder.flush()?;
370
371                    let b = decoder.get_mut().take();
372                    if !b.is_empty() {
373                        Ok(Some(b))
374                    } else {
375                        Ok(None)
376                    }
377                }
378                Err(err) => Err(err),
379            },
380        }
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use futures_util::{stream, StreamExt as _};
387    use rand::RngExt as _;
388
389    use super::*;
390
391    const ENCODINGS: &[ContentEncoding] = &[
392        #[cfg(feature = "compress-brotli")]
393        ContentEncoding::Brotli,
394        #[cfg(feature = "compress-gzip")]
395        ContentEncoding::Gzip,
396        #[cfg(feature = "compress-gzip")]
397        ContentEncoding::Deflate,
398        #[cfg(feature = "compress-zstd")]
399        ContentEncoding::Zstd,
400    ];
401
402    fn encode(data: &[u8], encoding: ContentEncoding) -> Bytes {
403        match encoding {
404            #[cfg(feature = "compress-brotli")]
405            ContentEncoding::Brotli => {
406                let mut encoder = brotli::CompressorWriter::new(Vec::new(), 4096, 3, 22);
407                encoder.write_all(data).unwrap();
408                encoder.into_inner().into()
409            }
410
411            #[cfg(feature = "compress-gzip")]
412            ContentEncoding::Gzip => {
413                let mut encoder =
414                    flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
415                encoder.write_all(data).unwrap();
416                encoder.finish().unwrap().into()
417            }
418
419            #[cfg(feature = "compress-gzip")]
420            ContentEncoding::Deflate => {
421                let mut encoder =
422                    flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
423                encoder.write_all(data).unwrap();
424                encoder.finish().unwrap().into()
425            }
426
427            #[cfg(feature = "compress-zstd")]
428            ContentEncoding::Zstd => zstd::stream::encode_all(data, 3).unwrap().into(),
429
430            ContentEncoding::Identity => Bytes::copy_from_slice(data),
431        }
432    }
433
434    #[actix_rt::test]
435    async fn decoded_output_is_bounded() {
436        let data = vec![b'a'; 1024 * 1024];
437
438        for &encoding in ENCODINGS {
439            let compressed = encode(&data, encoding);
440            let stream = stream::iter([Ok(compressed)]);
441            let mut decoder = Decoder::new(stream, encoding).limit(1024);
442
443            assert!(
444                matches!(decoder.next().await, Some(Err(PayloadError::Overflow))),
445                "{encoding:?} should report overflow"
446            );
447            assert!(
448                decoder.next().await.is_none(),
449                "{encoding:?} should end after overflow"
450            );
451        }
452    }
453
454    #[actix_rt::test]
455    async fn blocking_decode_reports_overflow() {
456        let data_that_compresses_poorly = rand::rng()
457            .sample_iter(rand::distr::StandardUniform)
458            .take(65_536)
459            .collect::<Vec<u8>>();
460
461        for &encoding in ENCODINGS {
462            let compressed = encode(&data_that_compresses_poorly, encoding);
463
464            assert!(
465                compressed.len() >= MAX_CHUNK_SIZE_DECODE_IN_PLACE,
466                "{encoding:?} should use the blocking decode path"
467            );
468
469            let stream = stream::iter([Ok(compressed)]);
470            let mut decoder = Decoder::new(stream, encoding).limit(1024);
471
472            assert!(
473                matches!(decoder.next().await, Some(Err(PayloadError::Overflow))),
474                "{encoding:?} should report overflow"
475            );
476            assert!(
477                decoder.next().await.is_none(),
478                "{encoding:?} should end after overflow"
479            );
480        }
481    }
482
483    #[actix_rt::test]
484    async fn decoded_output_at_limit_is_complete() {
485        let data = vec![b'a'; 1024];
486
487        for &encoding in ENCODINGS {
488            let compressed = encode(&data, encoding);
489            let stream = stream::iter(
490                compressed
491                    .chunks(8)
492                    .map(|chunk| Ok(Bytes::copy_from_slice(chunk)))
493                    .collect::<Vec<_>>(),
494            );
495
496            let mut decoder = Decoder::new(stream, encoding).limit(data.len());
497            let mut output = Vec::new();
498
499            while let Some(chunk) = decoder.next().await {
500                output.extend_from_slice(&chunk.unwrap());
501            }
502
503            assert_eq!(
504                output, data,
505                "{encoding:?} should decode the complete body at the limit"
506            );
507        }
508    }
509
510    #[actix_rt::test]
511    async fn decoded_limit_applies_across_chunks() {
512        let data = vec![b'a'; 1024 * 1024];
513
514        for &encoding in ENCODINGS {
515            let compressed = encode(&data, encoding);
516            let chunks = compressed
517                .chunks(16)
518                .map(|chunk| Ok(Bytes::copy_from_slice(chunk)))
519                .collect::<Vec<_>>();
520
521            let mut decoder = Decoder::new(stream::iter(chunks), encoding).limit(200_000);
522            let mut size = 0;
523
524            loop {
525                match decoder.next().await {
526                    Some(Ok(chunk)) => {
527                        size += chunk.len();
528                        assert!(
529                            size <= 200_000,
530                            "{encoding:?} should keep total decoded output within the limit"
531                        );
532                    }
533                    Some(Err(PayloadError::Overflow)) => break,
534                    result => panic!("{encoding:?} should report overflow, got {result:?}"),
535                }
536            }
537
538            assert!(
539                decoder.next().await.is_none(),
540                "{encoding:?} should end after overflow"
541            );
542        }
543    }
544
545    #[actix_rt::test]
546    async fn zero_decoded_limit_rejects_nonempty_body() {
547        for &encoding in ENCODINGS {
548            let compressed = encode(b"a", encoding);
549            let mut decoder = Decoder::new(stream::iter([Ok(compressed)]), encoding).limit(0);
550            assert!(
551                matches!(decoder.next().await, Some(Err(PayloadError::Overflow))),
552                "{encoding:?} should report overflow"
553            );
554        }
555    }
556
557    #[test]
558    fn io_errors_are_not_classified_as_overflow() {
559        assert!(matches!(
560            from_decoder_error(io::Error::other("codec error")),
561            PayloadError::Incomplete(Some(_))
562        ));
563    }
564}