Describe the bug
Hijack() has different preconditions on the two transports.
HTTP/2, http2RespWriter.Hijack in connection/http2.go, refuses when no status was written yet:
if !rp.statusWritten {
return nil, nil, fmt.Errorf("status not yet written before attempting to hijack connection")
}
QUIC, httpResponseAdapter.Hijack in connection/quic_connection.go, has no such check. It always returns a localProxyConnection, even when connectResponseSent is still false, so the caller can write raw bytes to the stream before any connect response went out.
Same caller, different outcome depending on the transport: on HTTP/2 it gets an error, on QUIC it gets a conn.
To Reproduce
Stock cloudflared writes the status before it hijacks, so the built-in ingress path does not hit this. I hit it in a fork that replaces the OriginProxy, with code that hijacks first. net/http/httputil.ReverseProxy does exactly that on a 101: handleUpgradeResponse calls Hijack() and then writes the status line onto the conn itself.
ProxyHTTP serves the request through httputil.ReverseProxy to a backend that answers 101.
- With
--protocol http2, Hijack fails and the client gets a 502.
- With
--protocol quic, Hijack succeeds and the caller can write to the stream before any connect response was sent.
Expected behavior
Both transports enforce the same precondition. The HTTP/2 check looks like the right one, so QUIC could refuse the same way while connectResponseSent is false.
Environment and versions
- OS: Linux
- Architecture: AMD64
- Version: 2026.9.1, and the
Hijack bodies are unchanged on master as of 2026.9.3
Logs and errors
HTTP/2: status not yet written before attempting to hijack connection. QUIC: no error.
Additional context
I worked around it on my side, so it does not block me. Filing it because the two transports disagree on the ResponseWriter contract, and that is easy to miss.
Describe the bug
Hijack()has different preconditions on the two transports.HTTP/2,
http2RespWriter.Hijackinconnection/http2.go, refuses when no status was written yet:QUIC,
httpResponseAdapter.Hijackinconnection/quic_connection.go, has no such check. It always returns alocalProxyConnection, even whenconnectResponseSentis still false, so the caller can write raw bytes to the stream before any connect response went out.Same caller, different outcome depending on the transport: on HTTP/2 it gets an error, on QUIC it gets a conn.
To Reproduce
Stock cloudflared writes the status before it hijacks, so the built-in ingress path does not hit this. I hit it in a fork that replaces the
OriginProxy, with code that hijacks first.net/http/httputil.ReverseProxydoes exactly that on a 101:handleUpgradeResponsecallsHijack()and then writes the status line onto the conn itself.ProxyHTTPserves the request throughhttputil.ReverseProxyto a backend that answers 101.--protocol http2,Hijackfails and the client gets a 502.--protocol quic,Hijacksucceeds and the caller can write to the stream before any connect response was sent.Expected behavior
Both transports enforce the same precondition. The HTTP/2 check looks like the right one, so QUIC could refuse the same way while
connectResponseSentis false.Environment and versions
Hijackbodies are unchanged on master as of 2026.9.3Logs and errors
HTTP/2:
status not yet written before attempting to hijack connection. QUIC: no error.Additional context
I worked around it on my side, so it does not block me. Filing it because the two transports disagree on the
ResponseWritercontract, and that is easy to miss.