Skip to main content

rama_http/layer/remove_header/
mod.rs

1//! Middleware for removing headers from requests and responses.
2//!
3//! See [request] and [response] for more details.
4
5use rama_core::telemetry::tracing;
6use rama_http_headers::{Connection, HeaderMapExt};
7use rama_utils::str::{any_submatch_ignore_ascii_case, starts_with_ignore_ascii_case};
8
9use crate::{HeaderMap, HeaderName, HeaderValue, header};
10
11pub mod request;
12pub mod response;
13
14#[doc(inline)]
15pub use self::{
16    request::{RemoveRequestHeader, RemoveRequestHeaderLayer},
17    response::{RemoveResponseHeader, RemoveResponseHeaderLayer},
18};
19
20fn remove_headers_by_prefix(headers: &mut HeaderMap, prefix: &str) {
21    let keys: Vec<_> = headers
22        .keys()
23        // this assumes that `HeaderName::as_str` returns as lowercase
24        .filter(|key| starts_with_ignore_ascii_case(key, prefix))
25        .cloned()
26        .collect();
27
28    for key in keys {
29        headers.remove(key);
30    }
31}
32
33fn remove_headers_by_exact_name(headers: &mut HeaderMap, name: &HeaderName) {
34    headers.remove(name);
35}
36
37/// Remove hop by hop headers from an outbound request.
38///
39/// This function applies the rules from RFC 9110 for hop by hop headers
40/// before forwarding a request to another hop.
41///
42/// This should be called when acting as a forward proxy, reverse proxy,
43/// or gateway that forwards requests to an upstream server.
44pub fn remove_hop_by_hop_request_headers(headers: &mut HeaderMap) {
45    while let Some(c) = headers.typed_get::<Connection>() {
46        for header in c.iter_headers() {
47            while headers.remove(header).is_some() {
48                tracing::trace!(
49                    "removed hop-by-hop request header listed in Connection header for name: {header}"
50                );
51            }
52        }
53        _ = headers.remove(header::CONNECTION);
54    }
55    for header in [
56        &header::CONNECTION,
57        &header::PROXY_CONNECTION,
58        &header::PROXY_AUTHORIZATION,
59        &header::TE,
60        &header::TRAILER,
61        &header::TRANSFER_ENCODING,
62        &header::UPGRADE,
63        &header::X_FORWARDED_FOR,
64        &header::X_FORWARDED_HOST,
65        &header::X_FORWARDED_PROTO,
66        &header::FORWARDED,
67        &header::VIA,
68        &header::CF_CONNECTING_IP,
69        &header::X_REAL_IP,
70        &header::X_CLIENT_IP,
71        &header::CLIENT_IP,
72        &header::TRUE_CLIENT_IP,
73    ] {
74        while headers.remove(header).is_some() {
75            tracing::trace!("removed hop-by-hop request header for name: {header}");
76        }
77    }
78}
79
80/// Remove hop by hop headers from an outbound response.
81///
82/// This function applies the rules from RFC 9110 for hop by hop headers
83/// before forwarding a response to a downstream client.
84///
85/// This should be called when relaying responses received from an upstream
86/// server to a client.
87pub fn remove_hop_by_hop_response_headers(headers: &mut HeaderMap) {
88    while let Some(c) = headers.typed_get::<Connection>() {
89        for header in c.iter_headers() {
90            while headers.remove(header).is_some() {
91                tracing::trace!(
92                    "removed hop-by-hop response header listed in Connection header for name: {header}"
93                );
94            }
95        }
96        _ = headers.remove(header::CONNECTION);
97    }
98    for header in [
99        &header::CONNECTION,
100        &header::KEEP_ALIVE,
101        &header::PROXY_AUTHENTICATE,
102        &header::TRAILER,
103        &header::TRANSFER_ENCODING,
104        &header::UPGRADE,
105    ] {
106        while headers.remove(header).is_some() {
107            tracing::trace!("removed hop-by-hop response header for name: {header}");
108        }
109    }
110}
111
112/// Remove headers that are illegal on an HTTP/2 (or HTTP/3) request.
113///
114/// HTTP/2 forbids connection-specific (hop-by-hop) header fields: the only
115/// exception is `TE`, and even then only with the value `trailers`
116/// (RFC 9113 §8.2.2). This removes the connection-specific headers (including
117/// any named by a `Connection` header), plus `Host` (replaced by the
118/// `:authority` pseudo-header) and `Sec-WebSocket-Key` (unused in the HTTP/2
119/// WebSocket handshake per RFC 8441 §5.1).
120pub fn remove_illegal_h2_request_headers(headers: &mut HeaderMap) {
121    while let Some(c) = headers.typed_get::<Connection>() {
122        for header in c.iter_headers() {
123            while headers.remove(header).is_some() {
124                tracing::trace!(
125                    header = %header,
126                    "removed connection-specific request header listed in Connection header for name"
127                );
128            }
129        }
130        _ = headers.remove(header::CONNECTION);
131    }
132    for header in [
133        &header::CONNECTION,
134        &header::PROXY_CONNECTION,
135        &header::KEEP_ALIVE,
136        &header::TRANSFER_ENCODING,
137        &header::UPGRADE,
138        &header::SEC_WEBSOCKET_KEY,
139        &header::HOST,
140    ] {
141        while headers.remove(header).is_some() {
142            tracing::trace!(
143                header = %header,
144                "removed illegal (~http1) header from h2 request for name"
145            );
146        }
147    }
148
149    // `TE` is the one connection-specific header permitted in HTTP/2 and HTTP/3, but
150    // only with the exact value `trailers` (RFC 9113 §8.2.2). Strip any other use.
151    let te_is_legal = headers
152        .get_all(header::TE)
153        .iter()
154        .all(|v| v.as_bytes().trim_ascii().eq_ignore_ascii_case(b"trailers"));
155    if !te_is_legal {
156        while headers.remove(header::TE).is_some() {
157            tracing::trace!(
158                "removed illegal TE header (only `TE: trailers` is valid) from h2 request"
159            );
160        }
161    }
162}
163
164/// Remove headers that are illegal on an HTTP/2 (or HTTP/3) response.
165///
166/// HTTP/2 forbids connection-specific (hop-by-hop) header fields (RFC 9113 §8.2.2).
167/// This removes only those headers (including any named by a `Connection` header) so
168/// that a response can be (re)serialized over HTTP/2.
169///
170/// Unlike [`remove_hop_by_hop_response_headers`], this is a pure protocol-legality
171/// operation, not a proxy forwarding policy: it leaves headers that are perfectly
172/// legal in HTTP/2 such as `Trailer` and `Proxy-Authenticate`. Use this when merely
173/// changing a message's HTTP version (which may happen on the same server, with no
174/// downstream hop), and use `remove_hop_by_hop_response_headers` when actually
175/// relaying a response across a connection hop.
176pub fn remove_illegal_h2_response_headers(headers: &mut HeaderMap) {
177    while let Some(c) = headers.typed_get::<Connection>() {
178        for header in c.iter_headers() {
179            while headers.remove(header).is_some() {
180                tracing::trace!(
181                    header = %header,
182                    "removed connection-specific response header listed in Connection header for name"
183                );
184            }
185        }
186        _ = headers.remove(header::CONNECTION);
187    }
188    for header in [
189        &header::CONNECTION,
190        &header::PROXY_CONNECTION,
191        &header::KEEP_ALIVE,
192        &header::TRANSFER_ENCODING,
193        &header::UPGRADE,
194    ] {
195        while headers.remove(header).is_some() {
196            tracing::trace!(
197                header = %header,
198                "removed illegal (~http1) header from h2 response for name"
199            );
200        }
201    }
202}
203
204/// Remove sensitive headers from an outbound request.
205///
206/// This function removes headers that may contain credentials,
207/// authentication material, or security tokens.
208///
209/// This is typically used when:
210/// - Forwarding requests across trust boundaries
211/// - Logging or persisting request metadata
212/// - Sending requests to untrusted upstreams
213pub fn remove_sensitive_request_headers(headers: &mut HeaderMap) {
214    for header in [
215        &header::AUTHORIZATION,
216        &header::PROXY_AUTHORIZATION,
217        &header::COOKIE,
218    ] {
219        while headers.remove(header).is_some() {
220            tracing::trace!("removed sensitive request header for name: {header}");
221        }
222    }
223    remove_headers_if(
224        headers,
225        |name, _value| is_sensitive_header_name(name),
226        "sensitive request header",
227    );
228}
229
230/// Remove sensitive headers from an outbound response.
231///
232/// This function removes headers that may expose session identifiers
233/// or user specific state.
234///
235/// This is typically used when responses should not propagate
236/// authentication state or tracking information.
237pub fn remove_sensitive_response_headers(headers: &mut HeaderMap) {
238    for header in [&header::SET_COOKIE] {
239        while headers.remove(header).is_some() {
240            tracing::trace!("removed sensitive response header for name: {header}");
241        }
242    }
243}
244
245/// Remove headers that describe or affect payload framing.
246///
247/// This function removes headers that are no longer valid when the
248/// payload has been transformed, reencoded, or regenerated.
249///
250/// This should be called after modifying a request or response body,
251/// such as decompression, aggregation, or content rewriting.
252pub fn remove_payload_metadata_headers(headers: &mut HeaderMap) {
253    for header in [
254        &header::CONTENT_ENCODING,
255        &header::TRANSFER_ENCODING,
256        &header::ACCEPT_RANGES,
257        &header::CONTENT_LENGTH,
258    ] {
259        while headers.remove(header).is_some() {
260            tracing::trace!("removed payload header for name: {header}");
261        }
262    }
263}
264
265/// Remove cache validation and conditional request headers.
266///
267/// These headers influence conditional requests and partial responses.
268/// They are typically removed when the proxy may change representation
269/// semantics or body bytes, or when the proxy wants to force a fresh
270/// upstream response.
271///
272/// Call this when you rewrite, decompress, aggregate, or otherwise
273/// transform the response body, or when you want to disable conditional
274/// requests through this hop.
275pub fn remove_cache_validation_request_headers(headers: &mut HeaderMap) {
276    for header in [
277        &header::IF_NONE_MATCH,
278        &header::IF_MODIFIED_SINCE,
279        &header::IF_MATCH,
280        &header::IF_UNMODIFIED_SINCE,
281        &header::IF_RANGE,
282        &header::RANGE,
283    ] {
284        while headers.remove(header).is_some() {
285            tracing::trace!("removed cache validation request header for name: {header}");
286        }
287    }
288}
289
290/// Remove cache validators and representation range metadata from a response.
291///
292/// These headers describe validators or byte range capabilities of the
293/// response representation. They may become invalid if the response body
294/// is transformed, reencoded, or regenerated.
295///
296/// Call this after changing the response body, changing content encoding,
297/// or otherwise making the downstream representation differ from the
298/// upstream representation.
299pub fn remove_cache_validation_response_headers(headers: &mut HeaderMap) {
300    for header in [
301        &header::ETAG,
302        &header::LAST_MODIFIED,
303        &header::ACCEPT_RANGES,
304        &header::CONTENT_RANGE,
305    ] {
306        while headers.remove(header).is_some() {
307            tracing::trace!("removed cache validation response header for name: {header}");
308        }
309    }
310}
311
312/// Remove caching policy headers.
313///
314/// These headers control how requests and responses may be cached by
315/// clients and intermediaries. Removing them can be useful when the proxy
316/// wants to enforce its own caching policy or prevent caching entirely.
317///
318/// Call this when you want to disable or normalize caching behavior
319/// across a trust boundary.
320pub fn remove_cache_policy_headers(headers: &mut HeaderMap) {
321    for header in [
322        &header::CACHE_CONTROL,
323        &header::PRAGMA,
324        &header::EXPIRES,
325        &header::AGE,
326        &header::WARNING,
327    ] {
328        while headers.remove(header).is_some() {
329            tracing::trace!("removed cache policy header for name: {header}");
330        }
331    }
332}
333
334#[inline(always)]
335fn is_sensitive_header_name(name: &HeaderName) -> bool {
336    any_submatch_ignore_ascii_case(
337        name.as_str(),
338        ["api-key", "auth-token", "access-token", "security-token"],
339    )
340}
341
342fn remove_headers_if<F>(headers: &mut HeaderMap, mut remove: F, log_context: &str)
343where
344    F: FnMut(&HeaderName, &HeaderValue) -> bool,
345{
346    loop {
347        let name_to_remove: Option<HeaderName> = headers
348            .iter()
349            .find_map(|(name, value)| remove(name, value).then(|| name.clone()));
350
351        let Some(name) = name_to_remove else { break };
352
353        while headers.remove(&name).is_some() {
354            tracing::trace!("{log_context}: removed header: {name}");
355        }
356    }
357}