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}