Skip to content

WebSocket header casing normalization reverted when header ops are configured #7784

@AliMickey

Description

@AliMickey

Issue Details

The RFC 6455 WebSocket header casing added by normalizeWebsocketHeaders (from #6621) gets reverted to Go's canonical casing before the request is sent upstream, whenever the reverse_proxy has request header operations. Upstreams that compare these header names case-sensitively reject the handshake (I know the consensus would be to fix the application, however this is not always possible as my use case is to proxy server's BMC interfaces which most are EOL and unsupported).

This happens when any header_up is configured, or the upstream is HTTPS (the transport injects a Host op automatically). So wss:// to an HTTPS backend is affected even with no explicit header config.

prepareRequest normalizes the casing correctly (Sec-WebSocket-Key). But proxyLoopIteration then rebuilds the header map with copyHeader (which uses Header.Add → CanonicalHeaderKey), turning it back into Sec-Websocket-Key, and nothing re-normalizes afterward:

Go's http.Transport writes map keys verbatim, so the canonical (non-RFC) casing reaches the wire.

The response upgrade path already gets this right, the request path is just missing the same step.

Suggested fix
Re-run normalizeWebsocketHeaders(r.Header) at the end of that rebuild block, mirroring the response path.

https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/reverseproxy.go#L686-L700

if transportOps != nil || userOps != nil {
    r.Header = make(http.Header)
    copyHeader(r.Header, reqHeader)
    r.Host = reqHost
    if transportOps != nil {
        transportOps.ApplyToRequest(r)
    }
    if userOps != nil {
        userOps.ApplyToRequest(r)
    }
    normalizeWebsocketHeaders(r.Header)
}

Assistance Disclosure

AI used

If AI was used, describe the extent to which it was used.

Was used to debug my original issue with caddy and proxying BMC interfaces, and to draft this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug 🐞Something isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions