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::bytes::BytesMut;
6use rama_core::telemetry::tracing;
7use rama_http_headers::{Connection, HeaderMapExt};
8use rama_utils::str::{any_submatch_ignore_ascii_case, starts_with_ignore_ascii_case};
9
10use crate::{HeaderMap, HeaderName, HeaderValue, header};
11
12pub mod request;
13pub mod response;
14
15#[doc(inline)]
16pub use self::{
17    request::{RemoveRequestHeader, RemoveRequestHeaderLayer},
18    response::{RemoveResponseHeader, RemoveResponseHeaderLayer},
19};
20
21fn remove_headers_by_prefix(headers: &mut HeaderMap, prefix: &str) {
22    let keys: Vec<_> = headers
23        .keys()
24        // this assumes that `HeaderName::as_str` returns as lowercase
25        .filter(|key| starts_with_ignore_ascii_case(key, prefix))
26        .cloned()
27        .collect();
28
29    for key in keys {
30        headers.remove(key);
31    }
32}
33
34fn remove_headers_by_exact_name(headers: &mut HeaderMap, name: &HeaderName) {
35    headers.remove(name);
36}
37
38/// Remove hop by hop headers from an outbound request.
39///
40/// This function applies the rules from RFC 9110 for hop by hop headers
41/// before forwarding a request to another hop.
42///
43/// This should be called when acting as a forward proxy, reverse proxy,
44/// or gateway that forwards requests to an upstream server.
45///
46/// End-to-end forwarding metadata is preserved unless explicitly named by the
47/// `Connection` header. Use [`remove_forwarding_metadata_request_headers`] when
48/// that metadata must be scrubbed as a separate policy decision.
49pub fn remove_hop_by_hop_request_headers(headers: &mut HeaderMap) {
50    while let Some(c) = headers.typed_get::<Connection>() {
51        for header in c.iter_headers() {
52            while headers.remove(header).is_some() {
53                tracing::trace!(
54                    "removed hop-by-hop request header listed in Connection header for name: {header}"
55                );
56            }
57        }
58        _ = headers.remove(header::CONNECTION);
59    }
60    for header in [
61        &header::CONNECTION,
62        &header::PROXY_CONNECTION,
63        &header::PROXY_AUTHORIZATION,
64        &header::KEEP_ALIVE,
65        &header::TE,
66        &header::TRAILER,
67        &header::TRANSFER_ENCODING,
68        &header::UPGRADE,
69    ] {
70        while headers.remove(header).is_some() {
71            tracing::trace!("removed hop-by-hop request header for name: {header}");
72        }
73    }
74}
75
76/// Remove request headers that describe the forwarding chain or client address.
77///
78/// Unlike hop-by-hop headers, these fields are normally preserved or appended to by
79/// intermediaries. Remove them explicitly when crossing a trust boundary or before
80/// deriving fresh forwarding metadata. This includes `Forwarded`, `Via`, the
81/// `X-Forwarded-*` family, and common client-IP aliases.
82///
83/// This function only modifies the supplied header map. Forwarding information
84/// already parsed into request extensions is unaffected.
85pub fn remove_forwarding_metadata_request_headers(headers: &mut HeaderMap) {
86    for header in [
87        &header::X_FORWARDED_FOR,
88        &header::X_FORWARDED_HOST,
89        &header::X_FORWARDED_PROTO,
90        &header::FORWARDED,
91        &header::VIA,
92        &header::CF_CONNECTING_IP,
93        &header::X_REAL_IP,
94        &header::X_CLIENT_IP,
95        &header::CLIENT_IP,
96        &header::TRUE_CLIENT_IP,
97    ] {
98        while headers.remove(header).is_some() {
99            tracing::trace!("removed forwarding metadata request header for name: {header}");
100        }
101    }
102}
103
104/// Remove hop by hop headers from an outbound response.
105///
106/// This function applies the rules from RFC 9110 for hop by hop headers
107/// before forwarding a response to a downstream client.
108///
109/// This should be called when relaying responses received from an upstream
110/// server to a client.
111pub fn remove_hop_by_hop_response_headers(headers: &mut HeaderMap) {
112    while let Some(c) = headers.typed_get::<Connection>() {
113        for header in c.iter_headers() {
114            while headers.remove(header).is_some() {
115                tracing::trace!(
116                    "removed hop-by-hop response header listed in Connection header for name: {header}"
117                );
118            }
119        }
120        _ = headers.remove(header::CONNECTION);
121    }
122    for header in [
123        &header::CONNECTION,
124        &header::KEEP_ALIVE,
125        &header::PROXY_AUTHENTICATE,
126        &header::TRAILER,
127        &header::TRANSFER_ENCODING,
128        &header::UPGRADE,
129    ] {
130        while headers.remove(header).is_some() {
131            tracing::trace!("removed hop-by-hop response header for name: {header}");
132        }
133    }
134}
135
136/// Coalesce multiple `Cookie` request header fields into a single field.
137///
138/// Values retain their original order and are joined using `; `, matching
139/// [RFC 6265 section 5.4](https://github.com/plabayo/rama/blob/main/rama-http/specifications/rfc6265.txt#section-5.4)
140/// serialization and [RFC 9113 section 8.2.3](https://github.com/plabayo/rama/blob/main/rama-http-core/specifications/rfc9113.txt#section-8.2.3)
141/// reassembly rules.
142pub fn coalesce_cookie_headers(headers: &mut HeaderMap) {
143    let crate::header::Entry::Occupied(mut cookie_headers) = headers.entry(header::COOKIE) else {
144        return;
145    };
146
147    let Some((bytes_count, header_count, is_sensitive)) = cookie_headers
148        .iter()
149        .map(|value| (value.as_bytes().len(), 1usize, value.is_sensitive()))
150        .reduce(|a, b| (a.0 + b.0, a.1 + b.1, a.2 || b.2))
151    else {
152        return;
153    };
154    if header_count <= 1 {
155        return;
156    }
157
158    let mut buffer = BytesMut::with_capacity(bytes_count + ((header_count - 1) * 2));
159    let mut header_values = cookie_headers.iter();
160    if let Some(header_value) = header_values.next() {
161        buffer.extend_from_slice(header_value.as_bytes());
162    }
163    for header_value in header_values {
164        buffer.extend_from_slice(b"; ");
165        buffer.extend_from_slice(header_value.as_bytes());
166    }
167
168    // Every input is already a valid HeaderValue and the separator is valid as well.
169    let Ok(mut header_value) = HeaderValue::from_maybe_shared(buffer.freeze()) else {
170        tracing::error!("failed to coalesce valid Cookie header values");
171        return;
172    };
173    header_value.set_sensitive(is_sensitive);
174    cookie_headers.insert(header_value);
175}
176
177/// Remove headers that are illegal on an HTTP/2 (or HTTP/3) request.
178///
179/// HTTP/2 forbids connection-specific (hop-by-hop) header fields: the only
180/// exception is `TE`, and even then only with the value `trailers`
181/// (RFC 9113 §8.2.2). This removes the connection-specific headers (including
182/// any named by a `Connection` header), plus `Host` (replaced by the
183/// `:authority` pseudo-header) and `Sec-WebSocket-Key` (unused in the HTTP/2
184/// WebSocket handshake per RFC 8441 §5.1).
185pub fn remove_illegal_h2_request_headers(headers: &mut HeaderMap) {
186    while let Some(c) = headers.typed_get::<Connection>() {
187        for header in c.iter_headers() {
188            while headers.remove(header).is_some() {
189                tracing::trace!(
190                    header = %header,
191                    "removed connection-specific request header listed in Connection header for name"
192                );
193            }
194        }
195        _ = headers.remove(header::CONNECTION);
196    }
197    for header in [
198        &header::CONNECTION,
199        &header::PROXY_CONNECTION,
200        &header::KEEP_ALIVE,
201        &header::TRANSFER_ENCODING,
202        &header::UPGRADE,
203        &header::SEC_WEBSOCKET_KEY,
204        &header::HOST,
205    ] {
206        while headers.remove(header).is_some() {
207            tracing::trace!(
208                header = %header,
209                "removed illegal (~http1) header from h2 request for name"
210            );
211        }
212    }
213
214    // `TE` is the one connection-specific header permitted in HTTP/2 and HTTP/3, but
215    // only with the exact value `trailers` (RFC 9113 §8.2.2). Strip any other use.
216    let te_is_legal = headers
217        .get_all(header::TE)
218        .iter()
219        .all(|v| v.as_bytes().trim_ascii().eq_ignore_ascii_case(b"trailers"));
220    if !te_is_legal {
221        while headers.remove(header::TE).is_some() {
222            tracing::trace!(
223                "removed illegal TE header (only `TE: trailers` is valid) from h2 request"
224            );
225        }
226    }
227}
228
229/// Remove headers that are illegal on an HTTP/2 (or HTTP/3) response.
230///
231/// HTTP/2 forbids connection-specific (hop-by-hop) header fields (RFC 9113 §8.2.2).
232/// This removes only those headers (including any named by a `Connection` header) so
233/// that a response can be (re)serialized over HTTP/2.
234///
235/// Unlike [`remove_hop_by_hop_response_headers`], this is a pure protocol-legality
236/// operation, not a proxy forwarding policy: it leaves headers that are perfectly
237/// legal in HTTP/2 such as `Trailer` and `Proxy-Authenticate`. Use this when merely
238/// changing a message's HTTP version (which may happen on the same server, with no
239/// downstream hop), and use `remove_hop_by_hop_response_headers` when actually
240/// relaying a response across a connection hop.
241pub fn remove_illegal_h2_response_headers(headers: &mut HeaderMap) {
242    while let Some(c) = headers.typed_get::<Connection>() {
243        for header in c.iter_headers() {
244            while headers.remove(header).is_some() {
245                tracing::trace!(
246                    header = %header,
247                    "removed connection-specific response header listed in Connection header for name"
248                );
249            }
250        }
251        _ = headers.remove(header::CONNECTION);
252    }
253    for header in [
254        &header::CONNECTION,
255        &header::PROXY_CONNECTION,
256        &header::KEEP_ALIVE,
257        &header::TRANSFER_ENCODING,
258        &header::UPGRADE,
259    ] {
260        while headers.remove(header).is_some() {
261            tracing::trace!(
262                header = %header,
263                "removed illegal (~http1) header from h2 response for name"
264            );
265        }
266    }
267}
268
269/// Remove sensitive headers from an outbound request.
270///
271/// This function removes headers that may contain credentials,
272/// authentication material, or security tokens.
273///
274/// This is typically used when:
275/// - Forwarding requests across trust boundaries
276/// - Logging or persisting request metadata
277/// - Sending requests to untrusted upstreams
278pub fn remove_sensitive_request_headers(headers: &mut HeaderMap) {
279    for header in [
280        &header::AUTHORIZATION,
281        &header::PROXY_AUTHORIZATION,
282        &header::COOKIE,
283    ] {
284        while headers.remove(header).is_some() {
285            tracing::trace!("removed sensitive request header for name: {header}");
286        }
287    }
288    remove_headers_if(
289        headers,
290        |name, _value| is_sensitive_header_name(name),
291        "sensitive request header",
292    );
293}
294
295/// Remove sensitive headers from an outbound response.
296///
297/// This function removes headers that may expose session identifiers
298/// or user specific state.
299///
300/// This is typically used when responses should not propagate
301/// authentication state or tracking information.
302pub fn remove_sensitive_response_headers(headers: &mut HeaderMap) {
303    for header in [&header::SET_COOKIE] {
304        while headers.remove(header).is_some() {
305            tracing::trace!("removed sensitive response header for name: {header}");
306        }
307    }
308}
309
310/// Remove headers that describe or affect payload framing.
311///
312/// This function removes headers that are no longer valid when the
313/// payload has been transformed, reencoded, or regenerated.
314///
315/// This should be called after modifying a request or response body,
316/// such as decompression, aggregation, or content rewriting.
317pub fn remove_payload_metadata_headers(headers: &mut HeaderMap) {
318    for header in [
319        &header::CONTENT_ENCODING,
320        &header::TRANSFER_ENCODING,
321        &header::ACCEPT_RANGES,
322        &header::CONTENT_LENGTH,
323    ] {
324        while headers.remove(header).is_some() {
325            tracing::trace!("removed payload header for name: {header}");
326        }
327    }
328}
329
330/// Remove cache validation and conditional request headers.
331///
332/// These headers influence conditional requests and partial responses.
333/// They are typically removed when the proxy may change representation
334/// semantics or body bytes, or when the proxy wants to force a fresh
335/// upstream response.
336///
337/// Call this when you rewrite, decompress, aggregate, or otherwise
338/// transform the response body, or when you want to disable conditional
339/// requests through this hop.
340pub fn remove_cache_validation_request_headers(headers: &mut HeaderMap) {
341    for header in [
342        &header::IF_NONE_MATCH,
343        &header::IF_MODIFIED_SINCE,
344        &header::IF_MATCH,
345        &header::IF_UNMODIFIED_SINCE,
346        &header::IF_RANGE,
347        &header::RANGE,
348    ] {
349        while headers.remove(header).is_some() {
350            tracing::trace!("removed cache validation request header for name: {header}");
351        }
352    }
353}
354
355/// Remove cache validators and representation range metadata from a response.
356///
357/// These headers describe validators or byte range capabilities of the
358/// response representation. They may become invalid if the response body
359/// is transformed, reencoded, or regenerated.
360///
361/// Call this after changing the response body, changing content encoding,
362/// or otherwise making the downstream representation differ from the
363/// upstream representation.
364pub fn remove_cache_validation_response_headers(headers: &mut HeaderMap) {
365    for header in [
366        &header::ETAG,
367        &header::LAST_MODIFIED,
368        &header::ACCEPT_RANGES,
369        &header::CONTENT_RANGE,
370    ] {
371        while headers.remove(header).is_some() {
372            tracing::trace!("removed cache validation response header for name: {header}");
373        }
374    }
375}
376
377/// Remove caching policy headers.
378///
379/// These headers control how requests and responses may be cached by
380/// clients and intermediaries. Removing them can be useful when the proxy
381/// wants to enforce its own caching policy or prevent caching entirely.
382///
383/// Call this when you want to disable or normalize caching behavior
384/// across a trust boundary.
385pub fn remove_cache_policy_headers(headers: &mut HeaderMap) {
386    for header in [
387        &header::CACHE_CONTROL,
388        &header::PRAGMA,
389        &header::EXPIRES,
390        &header::AGE,
391        &header::WARNING,
392    ] {
393        while headers.remove(header).is_some() {
394            tracing::trace!("removed cache policy header for name: {header}");
395        }
396    }
397}
398
399#[inline(always)]
400fn is_sensitive_header_name(name: &HeaderName) -> bool {
401    any_submatch_ignore_ascii_case(
402        name.as_str(),
403        ["api-key", "auth-token", "access-token", "security-token"],
404    )
405}
406
407fn remove_headers_if<F>(headers: &mut HeaderMap, mut remove: F, log_context: &str)
408where
409    F: FnMut(&HeaderName, &HeaderValue) -> bool,
410{
411    loop {
412        let name_to_remove: Option<HeaderName> = headers
413            .iter()
414            .find_map(|(name, value)| remove(name, value).then(|| name.clone()));
415
416        let Some(name) = name_to_remove else { break };
417
418        while headers.remove(&name).is_some() {
419            tracing::trace!("{log_context}: removed header: {name}");
420        }
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    const FORWARDING_METADATA_HEADERS: [&str; 10] = [
429        "x-forwarded-for",
430        "x-forwarded-host",
431        "x-forwarded-proto",
432        "forwarded",
433        "via",
434        "cf-connecting-ip",
435        "x-real-ip",
436        "x-client-ip",
437        "client-ip",
438        "true-client-ip",
439    ];
440
441    #[test]
442    fn hop_by_hop_request_cleanup_preserves_forwarding_metadata() {
443        let mut headers = HeaderMap::new();
444        for name in FORWARDING_METADATA_HEADERS {
445            headers.append(name, HeaderValue::from_static("metadata"));
446        }
447        headers.insert(header::CONNECTION, HeaderValue::from_static("x-hop"));
448        headers.insert("x-hop", HeaderValue::from_static("remove me"));
449        headers.insert(header::KEEP_ALIVE, HeaderValue::from_static("timeout=5"));
450        headers.insert(
451            header::PROXY_AUTHORIZATION,
452            HeaderValue::from_static("Basic dGVzdA=="),
453        );
454
455        remove_hop_by_hop_request_headers(&mut headers);
456
457        for name in FORWARDING_METADATA_HEADERS {
458            assert!(
459                headers.contains_key(name),
460                "expected {name} to be preserved"
461            );
462        }
463        for name in ["connection", "x-hop", "keep-alive", "proxy-authorization"] {
464            assert!(!headers.contains_key(name), "expected {name} to be removed");
465        }
466    }
467
468    #[test]
469    fn hop_by_hop_request_cleanup_removes_metadata_named_by_connection() {
470        let mut headers = HeaderMap::new();
471        headers.insert(header::CONNECTION, HeaderValue::from_static("forwarded"));
472        headers.insert(header::FORWARDED, HeaderValue::from_static("for=192.0.2.1"));
473        headers.insert(header::VIA, HeaderValue::from_static("1.1 proxy"));
474
475        remove_hop_by_hop_request_headers(&mut headers);
476
477        assert!(!headers.contains_key(header::FORWARDED));
478        assert!(headers.contains_key(header::VIA));
479    }
480
481    #[test]
482    fn forwarding_metadata_cleanup_is_independent() {
483        let mut headers = HeaderMap::new();
484        for name in FORWARDING_METADATA_HEADERS {
485            headers.append(name, HeaderValue::from_static("metadata"));
486        }
487        headers.insert(header::CONNECTION, HeaderValue::from_static("close"));
488
489        remove_forwarding_metadata_request_headers(&mut headers);
490
491        for name in FORWARDING_METADATA_HEADERS {
492            assert!(!headers.contains_key(name), "expected {name} to be removed");
493        }
494        assert!(headers.contains_key(header::CONNECTION));
495    }
496
497    #[test]
498    fn coalesce_cookies_preserves_order_and_sensitivity() {
499        let mut headers = HeaderMap::new();
500        headers.append(header::COOKIE, HeaderValue::from_static("a=1"));
501        let mut sensitive = HeaderValue::from_static("b=2");
502        sensitive.set_sensitive(true);
503        headers.append(header::COOKIE, sensitive);
504
505        coalesce_cookie_headers(&mut headers);
506
507        let cookies: Vec<_> = headers.get_all(header::COOKIE).iter().collect();
508        assert_eq!(cookies.len(), 1);
509        assert_eq!(cookies[0].as_bytes(), b"a=1; b=2");
510        assert!(cookies[0].is_sensitive());
511    }
512}