Skip to main content

actix_http/encoding/
encoder.rs

1//! Stream encoders.
2
3use std::{
4    error::Error as StdError,
5    future::Future,
6    io::{self, Write as _},
7    pin::Pin,
8    task::{Context, Poll},
9};
10
11use bytes::Bytes;
12use derive_more::Display;
13#[cfg(feature = "compress-gzip")]
14use flate2::write::{GzEncoder, ZlibEncoder};
15use futures_core::ready;
16use pin_project_lite::pin_project;
17use tokio::task::{spawn_blocking, JoinHandle};
18use tracing::trace;
19#[cfg(feature = "compress-zstd")]
20use zstd::stream::write::Encoder as ZstdEncoder;
21
22use super::Writer;
23use crate::{
24    body::{self, BodySize, MessageBody},
25    header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING},
26    ResponseHead, StatusCode,
27};
28
29const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
30
31pin_project! {
32    /// Response body that compresses chunks using a content encoding.
33    ///
34    /// Implements [`MessageBody`] and reports [`BodySize::Stream`] when compression is enabled.
35    /// Buffered output is flushed when the underlying body returns [`Poll::Pending`], and the
36    /// compressed stream is finalized when the body ends.
37    ///
38    /// See [`Encoder::response`] for conditions that skip compression and the
39    /// [module-level docs](crate::encoding) for supported encodings and runtime requirements.
40    pub struct Encoder<B> {
41        #[pin]
42        body: EncoderBody<B>,
43        encoder: Option<ContentEncoder>,
44        fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
45        needs_flush: bool,
46        eof: bool,
47    }
48}
49
50impl<B: MessageBody> Encoder<B> {
51    fn none() -> Self {
52        Encoder {
53            body: EncoderBody::None {
54                body: body::None::new(),
55            },
56            encoder: None,
57            fut: None,
58            needs_flush: false,
59            eof: true,
60        }
61    }
62
63    fn empty() -> Self {
64        Encoder {
65            body: EncoderBody::Full { body: Bytes::new() },
66            encoder: None,
67            fut: None,
68            needs_flush: false,
69            eof: true,
70        }
71    }
72
73    /// Constructs a response body encoder and updates the response head if compression is enabled.
74    ///
75    /// Compression is skipped when:
76    ///
77    /// - The body size is [`BodySize::None`] or [`BodySize::Sized(0)`](BodySize::Sized).
78    /// - The response already has a `Content-Encoding` header.
79    /// - The response status is 101 Switching Protocols, 204 No Content, or 206 Partial Content.
80    /// - `encoding` is [`ContentEncoding::Identity`] or its codec feature is disabled.
81    ///
82    /// Otherwise, sets `Content-Encoding`, appends `accept-encoding` to `Vary`, and enables chunked
83    /// transfer encoding in the response head. Existing `Content-Length` headers are not removed.
84    ///
85    /// The caller must select `encoding`; this method does not negotiate `Accept-Encoding`.
86    pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
87        // no need to compress empty bodies
88        match body.size() {
89            BodySize::None => return Self::none(),
90            BodySize::Sized(0) => return Self::empty(),
91            _ => {}
92        }
93
94        let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
95            || head.status == StatusCode::SWITCHING_PROTOCOLS
96            || head.status == StatusCode::NO_CONTENT
97            || head.status == StatusCode::PARTIAL_CONTENT
98            || encoding == ContentEncoding::Identity);
99
100        let body = match body.try_into_bytes() {
101            Ok(body) => EncoderBody::Full { body },
102            Err(body) => EncoderBody::Stream { body },
103        };
104
105        if should_encode {
106            // wrap body only if encoder is feature-enabled
107            if let Some(enc) = ContentEncoder::select(encoding) {
108                update_head(encoding, head);
109
110                return Encoder {
111                    body,
112                    encoder: Some(enc),
113                    fut: None,
114                    needs_flush: false,
115                    eof: false,
116                };
117            }
118        }
119
120        Encoder {
121            body,
122            encoder: None,
123            fut: None,
124            needs_flush: false,
125            eof: false,
126        }
127    }
128}
129
130pin_project! {
131    #[project = EncoderBodyProj]
132    enum EncoderBody<B> {
133        None { body: body::None },
134        Full { body: Bytes },
135        Stream { #[pin] body: B },
136    }
137}
138
139impl<B> MessageBody for EncoderBody<B>
140where
141    B: MessageBody,
142{
143    type Error = EncoderError;
144
145    #[inline]
146    fn size(&self) -> BodySize {
147        match self {
148            EncoderBody::None { body } => body.size(),
149            EncoderBody::Full { body } => body.size(),
150            EncoderBody::Stream { body } => body.size(),
151        }
152    }
153
154    fn poll_next(
155        self: Pin<&mut Self>,
156        cx: &mut Context<'_>,
157    ) -> Poll<Option<Result<Bytes, Self::Error>>> {
158        match self.project() {
159            EncoderBodyProj::None { body } => {
160                Pin::new(body).poll_next(cx).map_err(|err| match err {})
161            }
162            EncoderBodyProj::Full { body } => {
163                Pin::new(body).poll_next(cx).map_err(|err| match err {})
164            }
165            EncoderBodyProj::Stream { body } => body
166                .poll_next(cx)
167                .map_err(|err| EncoderError::Body(err.into())),
168        }
169    }
170
171    #[inline]
172    fn try_into_bytes(self) -> Result<Bytes, Self>
173    where
174        Self: Sized,
175    {
176        match self {
177            EncoderBody::None { body } => Ok(body.try_into_bytes().unwrap()),
178            EncoderBody::Full { body } => Ok(body.try_into_bytes().unwrap()),
179            _ => Err(self),
180        }
181    }
182}
183
184impl<B> MessageBody for Encoder<B>
185where
186    B: MessageBody,
187{
188    type Error = EncoderError;
189
190    #[inline]
191    fn size(&self) -> BodySize {
192        if self.encoder.is_some() {
193            BodySize::Stream
194        } else {
195            self.body.size()
196        }
197    }
198
199    fn poll_next(
200        self: Pin<&mut Self>,
201        cx: &mut Context<'_>,
202    ) -> Poll<Option<Result<Bytes, Self::Error>>> {
203        let mut this = self.project();
204
205        loop {
206            if *this.eof {
207                return Poll::Ready(None);
208            }
209
210            if let Some(ref mut fut) = this.fut {
211                let mut encoder = ready!(Pin::new(fut).poll(cx))
212                    .map_err(|_| {
213                        EncoderError::Io(io::Error::other(
214                            "Blocking task was cancelled unexpectedly",
215                        ))
216                    })?
217                    .map_err(EncoderError::Io)?;
218
219                let chunk = encoder.take();
220                *this.encoder = Some(encoder);
221                this.fut.take();
222
223                if !chunk.is_empty() {
224                    return Poll::Ready(Some(Ok(chunk)));
225                }
226            }
227
228            let result = match this.body.as_mut().poll_next(cx) {
229                Poll::Ready(result) => result,
230
231                Poll::Pending => {
232                    if *this.needs_flush {
233                        if let Some(encoder) = this.encoder.as_mut() {
234                            // Release buffered content when the producer pauses.
235                            encoder.flush().map_err(EncoderError::Io)?;
236                            *this.needs_flush = false;
237
238                            let chunk = encoder.take();
239
240                            if !chunk.is_empty() {
241                                return Poll::Ready(Some(Ok(chunk)));
242                            }
243                        }
244                    }
245
246                    return Poll::Pending;
247                }
248            };
249
250            match result {
251                Some(Err(err)) => return Poll::Ready(Some(Err(err))),
252
253                Some(Ok(chunk)) => {
254                    if let Some(mut encoder) = this.encoder.take() {
255                        *this.needs_flush |= !chunk.is_empty();
256
257                        if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE {
258                            encoder.write(&chunk).map_err(EncoderError::Io)?;
259                            let chunk = encoder.take();
260                            *this.encoder = Some(encoder);
261
262                            if !chunk.is_empty() {
263                                return Poll::Ready(Some(Ok(chunk)));
264                            }
265                        } else {
266                            *this.fut = Some(spawn_blocking(move || {
267                                encoder.write(&chunk)?;
268                                Ok(encoder)
269                            }));
270                        }
271                    } else {
272                        return Poll::Ready(Some(Ok(chunk)));
273                    }
274                }
275
276                None => {
277                    if let Some(encoder) = this.encoder.take() {
278                        let chunk = encoder.finish().map_err(EncoderError::Io)?;
279
280                        if chunk.is_empty() {
281                            return Poll::Ready(None);
282                        } else {
283                            *this.eof = true;
284                            return Poll::Ready(Some(Ok(chunk)));
285                        }
286                    } else {
287                        return Poll::Ready(None);
288                    }
289                }
290            }
291        }
292    }
293
294    #[inline]
295    fn try_into_bytes(mut self) -> Result<Bytes, Self>
296    where
297        Self: Sized,
298    {
299        if self.encoder.is_some() {
300            Err(self)
301        } else {
302            match self.body.try_into_bytes() {
303                Ok(body) => Ok(body),
304                Err(body) => {
305                    self.body = body;
306                    Err(self)
307                }
308            }
309        }
310    }
311}
312
313fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
314    head.headers_mut()
315        .insert(header::CONTENT_ENCODING, encoding.to_header_value());
316    head.headers_mut()
317        .append(header::VARY, HeaderValue::from_static("accept-encoding"));
318
319    head.no_chunking(false);
320}
321
322enum ContentEncoder {
323    #[cfg(feature = "compress-gzip")]
324    Deflate(ZlibEncoder<Writer>),
325
326    #[cfg(feature = "compress-gzip")]
327    Gzip(GzEncoder<Writer>),
328
329    #[cfg(feature = "compress-brotli")]
330    Brotli(Box<brotli::CompressorWriter<Writer>>),
331
332    // Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we
333    // use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
334    #[cfg(feature = "compress-zstd")]
335    Zstd(ZstdEncoder<'static, Writer>),
336}
337
338impl ContentEncoder {
339    fn select(encoding: ContentEncoding) -> Option<Self> {
340        match encoding {
341            #[cfg(feature = "compress-gzip")]
342            ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new(
343                Writer::new(),
344                flate2::Compression::fast(),
345            ))),
346
347            #[cfg(feature = "compress-gzip")]
348            ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new(
349                Writer::new(),
350                flate2::Compression::fast(),
351            ))),
352
353            #[cfg(feature = "compress-brotli")]
354            ContentEncoding::Brotli => Some(ContentEncoder::Brotli(new_brotli_compressor())),
355
356            #[cfg(feature = "compress-zstd")]
357            ContentEncoding::Zstd => {
358                let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
359                Some(ContentEncoder::Zstd(encoder))
360            }
361
362            _ => None,
363        }
364    }
365
366    #[inline]
367    pub(crate) fn take(&mut self) -> Bytes {
368        match *self {
369            #[cfg(feature = "compress-brotli")]
370            ContentEncoder::Brotli(ref mut encoder) => encoder.get_mut().take(),
371
372            #[cfg(feature = "compress-gzip")]
373            ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
374
375            #[cfg(feature = "compress-gzip")]
376            ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
377
378            #[cfg(feature = "compress-zstd")]
379            ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(),
380        }
381    }
382
383    fn flush(&mut self) -> Result<(), io::Error> {
384        match self {
385            #[cfg(feature = "compress-brotli")]
386            ContentEncoder::Brotli(encoder) => encoder.flush(),
387
388            #[cfg(feature = "compress-gzip")]
389            ContentEncoder::Gzip(encoder) => encoder.flush(),
390
391            #[cfg(feature = "compress-gzip")]
392            ContentEncoder::Deflate(encoder) => encoder.flush(),
393
394            #[cfg(feature = "compress-zstd")]
395            ContentEncoder::Zstd(encoder) => encoder.flush(),
396        }
397    }
398
399    fn finish(self) -> Result<Bytes, io::Error> {
400        match self {
401            #[cfg(feature = "compress-brotli")]
402            ContentEncoder::Brotli(mut encoder) => match encoder.flush() {
403                Ok(()) => Ok(encoder.into_inner().buf.freeze()),
404                Err(err) => Err(err),
405            },
406
407            #[cfg(feature = "compress-gzip")]
408            ContentEncoder::Gzip(encoder) => match encoder.finish() {
409                Ok(writer) => Ok(writer.buf.freeze()),
410                Err(err) => Err(err),
411            },
412
413            #[cfg(feature = "compress-gzip")]
414            ContentEncoder::Deflate(encoder) => match encoder.finish() {
415                Ok(writer) => Ok(writer.buf.freeze()),
416                Err(err) => Err(err),
417            },
418
419            #[cfg(feature = "compress-zstd")]
420            ContentEncoder::Zstd(encoder) => match encoder.finish() {
421                Ok(writer) => Ok(writer.buf.freeze()),
422                Err(err) => Err(err),
423            },
424        }
425    }
426
427    fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
428        match *self {
429            #[cfg(feature = "compress-brotli")]
430            ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
431                Ok(_) => Ok(()),
432                Err(err) => {
433                    trace!("Error encoding br data: {}", err);
434                    Err(err)
435                }
436            },
437
438            #[cfg(feature = "compress-gzip")]
439            ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
440                Ok(_) => Ok(()),
441                Err(err) => {
442                    trace!("Error encoding gzip data: {}", err);
443                    Err(err)
444                }
445            },
446
447            #[cfg(feature = "compress-gzip")]
448            ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
449                Ok(_) => Ok(()),
450                Err(err) => {
451                    trace!("Error encoding deflate data: {}", err);
452                    Err(err)
453                }
454            },
455
456            #[cfg(feature = "compress-zstd")]
457            ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
458                Ok(_) => Ok(()),
459                Err(err) => {
460                    trace!("Error encoding zstd data: {}", err);
461                    Err(err)
462                }
463            },
464        }
465    }
466}
467
468#[cfg(feature = "compress-brotli")]
469fn new_brotli_compressor() -> Box<brotli::CompressorWriter<Writer>> {
470    Box::new(brotli::CompressorWriter::new(
471        Writer::new(),
472        32 * 1024, // 32 KiB buffer
473        3,         // BROTLI_PARAM_QUALITY
474        22,        // BROTLI_PARAM_LGWIN
475    ))
476}
477
478/// Error returned while reading or compressing a response body.
479#[derive(Debug, Display)]
480#[non_exhaustive]
481pub enum EncoderError {
482    /// Wrapped body stream error.
483    #[display("Body")]
484    Body(Box<dyn StdError>),
485
486    /// Generic I/O error.
487    #[display("Io")]
488    Io(io::Error),
489}
490
491impl StdError for EncoderError {
492    fn source(&self) -> Option<&(dyn StdError + 'static)> {
493        match self {
494            EncoderError::Body(err) => Some(&**err),
495            EncoderError::Io(err) => Some(err),
496        }
497    }
498}
499
500impl From<EncoderError> for crate::Error {
501    fn from(err: EncoderError) -> Self {
502        crate::Error::new_encoder().with_cause(err)
503    }
504}