Skip to content

feat: add PreserveHost extension to skip Host header removal#177

Open
bearded-giant wants to merge 2 commits intoomjadas:mainfrom
bearded-giant:preserve-host
Open

feat: add PreserveHost extension to skip Host header removal#177
bearded-giant wants to merge 2 commits intoomjadas:mainfrom
bearded-giant:preserve-host

Conversation

@bearded-giant
Copy link
Copy Markdown

feat: add PreserveHost extension to opt out of Host header removal

Summary

This adds a PreserveHost marker type that can be inserted into request extensions during handle_request to prevent normalize_request from stripping the Host header. The current behavior (unconditional removal) is preserved as the default.

Problem

normalize_request unconditionally removes the Host header before forwarding:

fn normalize_request<T>(mut req: Request<T>) -> Request<T> {
    req.headers_mut().remove(hyper::header::HOST);
    // ...
}

This runs after HttpHandler::handle_request returns, so any Host header set by the handler is always discarded. Hyper then regenerates it from the URI authority.

For reverse proxy use cases where the request URI is rewritten to a local target (e.g. http://localhost:3000) but the upstream server needs the original Host header (e.g. Host: store.example.com) for virtual hosting, cookie domains, or redirect generation, there is no way to preserve it. The handler can set it, but normalize_request deletes it unconditionally.

Solution

A zero-cost opt-in using http::Extensions:

// new public type in lib.rs
#[derive(Debug, Clone, Copy)]
pub struct PreserveHost;
// in normalize_request
if req.extensions().get::<crate::PreserveHost>().is_none() {
    req.headers_mut().remove(hyper::header::HOST);
}

Usage in a handler:

fn handle_request(&mut self, _ctx: &HttpContext, req: Request<Body>) -> RequestOrResponse {
    let (mut parts, body) = req.into_parts();
    parts.uri = rewritten_uri;
    parts.extensions.insert(hudsucker::PreserveHost);
    Request::from_parts(parts, body).into()
}

Design choices

  • Extensions over builder flags: No changes to HttpHandler trait, ProxyBuilder, or any public API. Extensions are already the idiomatic hyper mechanism for passing per-request metadata through middleware.
  • Opt-in, not opt-out: Default behavior is unchanged. Existing users are unaffected. The Host header is only preserved when the handler explicitly requests it.
  • Zero cost when unused: A single Option::is_none() check on a TypeMap lookup. No allocations, no branching in the common path beyond the type check.

Tests

  • Existing removes_host_header test passes unchanged
  • New preserves_host_header_with_extension test verifies the opt-in path
  • All 20 existing tests + 6 doctests pass

Compatibility

  • No breaking changes
  • No new dependencies
  • No feature flag required
  • Backward compatible with all existing HttpHandler implementations

@bearded-giant bearded-giant marked this pull request as ready for review March 30, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant