Skip to main content

rama_http/layer/html_rewrite/
body.rs

1//! Streaming response [`Body`](crate::Body)
2//! that rewrites HTML on the fly.
3
4use std::pin::Pin;
5use std::task::{Context, Poll};
6
7use pin_project_lite::pin_project;
8use rama_core::bytes::{Buf, Bytes};
9use rama_core::error::BoxError;
10use rama_core::futures::ready;
11
12use crate::HeaderMap;
13use crate::body::{Frame, SizeHint, StreamingBody};
14use crate::protocols::html::rewrite::{ElementContentHandler, HtmlRewriter};
15use crate::protocols::html::selector::Selector;
16
17/// Completion hook, handed the finalized handler once the rewrite ends.
18/// `Send + Sync` so the body keeps satisfying [`Body::new`](crate::Body::new).
19type OnEnd<H> = Box<dyn FnOnce(H) + Send + Sync>;
20
21pin_project! {
22    /// A response body that feeds the inner body's bytes through an
23    /// [`HtmlRewriter`], emitting rewritten chunks as they become available.
24    ///
25    /// Build it directly with [`new`](Self::new) (rewriting) or
26    /// [`passthrough`](Self::passthrough) (forward unchanged), or let
27    /// [`HtmlRewriteLayer`](super::HtmlRewriteLayer) construct one per
28    /// response. Attach [`on_end`](Self::on_end) to recover the handler (and
29    /// any state it accumulated) once the rewrite finishes.
30    pub struct HtmlRewriteBody<B, H> {
31        #[pin]
32        inner: B,
33        // `None` => passthrough; `Some` => actively rewriting.
34        rewriter: Option<HtmlRewriter<H>>,
35        // Fired once after `end()` on clean termination; `None` => no hook.
36        on_end: Option<OnEnd<H>>,
37        pending_trailers: Option<HeaderMap>,
38        // Set once the inner body has ended and the rewriter is flushed.
39        done: bool,
40    }
41}
42
43impl<B, H> HtmlRewriteBody<B, H>
44where
45    H: ElementContentHandler,
46{
47    /// Wraps `inner`, rewriting elements matching `selectors` with `handler`
48    /// (the `selector` index passed to the handler is the index into
49    /// `selectors`).
50    pub fn new(inner: B, selectors: &[Selector], handler: H) -> Self {
51        Self {
52            inner,
53            rewriter: Some(HtmlRewriter::new(selectors, handler)),
54            on_end: None,
55            pending_trailers: None,
56            done: false,
57        }
58    }
59}
60
61impl<B, H> HtmlRewriteBody<B, H> {
62    /// Wraps `inner` without rewriting — frames pass through unchanged (their
63    /// data type normalized to [`Bytes`]).
64    ///
65    /// Lets a layer keep one body type for responses it must not rewrite
66    /// (e.g. a non-HTML content type).
67    pub fn passthrough(inner: B) -> Self {
68        Self {
69            inner,
70            rewriter: None,
71            on_end: None,
72            pending_trailers: None,
73            done: false,
74        }
75    }
76
77    /// Installs a completion hook, handed the finalized handler by value
78    /// after the rewrite ends — for reading state it accumulated.
79    ///
80    /// Fires once after [`HtmlRewriter::end`] on clean termination (inner EOF
81    /// or trailers); not on the error path, nor in
82    /// [`passthrough`](Self::passthrough) mode (no handler). A later call
83    /// replaces an earlier hook.
84    #[must_use]
85    pub fn on_end<F>(mut self, on_end: F) -> Self
86    where
87        F: FnOnce(H) + Send + Sync + 'static,
88    {
89        self.on_end = Some(Box::new(on_end));
90        self
91    }
92}
93
94/// Hands the spent rewriter's handler to the hook, if one is installed.
95fn fire_on_end<H: ElementContentHandler>(
96    rewriter: &mut Option<HtmlRewriter<H>>,
97    on_end: &mut Option<OnEnd<H>>,
98) {
99    if let (Some(rewriter), Some(on_end)) = (rewriter.take(), on_end.take()) {
100        on_end(rewriter.into_handler());
101    }
102}
103
104impl<B, H> StreamingBody for HtmlRewriteBody<B, H>
105where
106    B: StreamingBody<Error: Into<BoxError>>,
107    H: ElementContentHandler,
108{
109    type Data = Bytes;
110    type Error = BoxError;
111
112    fn poll_frame(
113        self: Pin<&mut Self>,
114        cx: &mut Context<'_>,
115    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
116        let mut this = self.project();
117
118        if let Some(trailers) = this.pending_trailers.take() {
119            *this.done = true;
120            return Poll::Ready(Some(Ok(Frame::trailers(trailers))));
121        }
122
123        if *this.done {
124            return Poll::Ready(None);
125        }
126
127        let Some(rewriter) = this.rewriter.as_mut() else {
128            // Passthrough: forward frames, normalizing the data type to `Bytes`.
129            return match ready!(this.inner.as_mut().poll_frame(cx)) {
130                Some(Ok(frame)) => Poll::Ready(Some(Ok(normalize_frame(frame)))),
131                Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
132                None => {
133                    *this.done = true;
134                    Poll::Ready(None)
135                }
136            };
137        };
138
139        loop {
140            match ready!(this.inner.as_mut().poll_frame(cx)) {
141                Some(Ok(frame)) => match frame.into_data() {
142                    Ok(mut data) => {
143                        // Feed the rewriter straight from the buffer's chunks:
144                        // the tokenizer copies what it needs into its own
145                        // buffer, so there is no intermediate `Bytes` copy.
146                        while data.has_remaining() {
147                            let chunk = data.chunk();
148                            let len = chunk.len();
149                            if let Err(err) = rewriter.write(chunk) {
150                                return Poll::Ready(Some(Err(err)));
151                            }
152                            data.advance(len);
153                        }
154                        let out = rewriter.take_output();
155                        if !out.is_empty() {
156                            return Poll::Ready(Some(Ok(Frame::data(Bytes::from(out)))));
157                        }
158                        // The rewriter buffered an incomplete construct (e.g. a
159                        // partial tag); keep polling for more input.
160                    }
161                    // A trailers frame terminates the body. Flush the
162                    // rewriter first so data never appears after trailers.
163                    Err(frame) => {
164                        if let Ok(trailers) = frame.into_trailers() {
165                            if let Err(err) = rewriter.end() {
166                                return Poll::Ready(Some(Err(err)));
167                            }
168                            let out = rewriter.take_output();
169                            fire_on_end(this.rewriter, this.on_end);
170                            if out.is_empty() {
171                                *this.done = true;
172                                return Poll::Ready(Some(Ok(Frame::trailers(trailers))));
173                            }
174                            *this.pending_trailers = Some(trailers);
175                            return Poll::Ready(Some(Ok(Frame::data(Bytes::from(out)))));
176                        }
177                    }
178                },
179                Some(Err(err)) => return Poll::Ready(Some(Err(err.into()))),
180                None => {
181                    *this.done = true;
182                    if let Err(err) = rewriter.end() {
183                        return Poll::Ready(Some(Err(err)));
184                    }
185                    let out = rewriter.take_output();
186                    fire_on_end(this.rewriter, this.on_end);
187                    return if out.is_empty() {
188                        Poll::Ready(None)
189                    } else {
190                        Poll::Ready(Some(Ok(Frame::data(Bytes::from(out)))))
191                    };
192                }
193            }
194        }
195    }
196
197    fn size_hint(&self) -> SizeHint {
198        if self.rewriter.is_some() {
199            // Rewriting changes the body length unpredictably.
200            SizeHint::default()
201        } else {
202            self.inner.size_hint()
203        }
204    }
205}
206
207/// Normalizes a frame's data type to [`Bytes`], preserving trailers.
208fn normalize_frame<D: Buf>(frame: Frame<D>) -> Frame<Bytes> {
209    match frame.into_data() {
210        Ok(mut data) => Frame::data(data.copy_to_bytes(data.remaining())),
211        Err(frame) => match frame.into_trailers() {
212            Ok(trailers) => Frame::trailers(trailers),
213            // `Frame` is data-or-trailers, so this is unreachable; emit an
214            // empty data frame rather than panic.
215            Err(_) => Frame::data(Bytes::new()),
216        },
217    }
218}