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