Description
When using Turnstile in front of a Streamlit application, WebSocket connections to /_stcore/stream fail with:
can't switch protocols using non-Hijacker ResponseWriter type *httpx.responseWriter
This results in a 502 on the WebSocket endpoint, causing a blank page in Streamlit (which requires WebSocket for its data stream).
Root Cause
internal/httpx/response.go defines a custom responseWriter that wraps http.ResponseWriter but does not implement http.Hijacker. When httputil.ReverseProxy (in internal/proxy/handler.go) tries to handle a WebSocket upgrade request, it needs to call Hijack() on the response writer to take over the TCP connection — but the wrapped writer doesn't support it.
Suggested Fix
Add Hijack() delegation to the custom response writer:
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := w.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, fmt.Errorf("upstream ResponseWriter does not implement http.Hijacker")
}
Environment
- Turnstile deployed via Railway template
- Backend: Streamlit (Python) on
frontend.railway.internal:8501
- WebSocket endpoint:
/_stcore/stream
- All other HTTP endpoints (health, host-config, static assets) proxy correctly
Logs
[ERRO] proxy error error="can't switch protocols using non-Hijacker ResponseWriter type *httpx.responseWriter" method="GET" path="/_stcore/stream"
[INFO] request method="GET" path="/_stcore/stream" status=502
Note: The README states "handles session management, WebSocket and SSE upgrades" — SSE works fine (FlushInterval=-1), but WebSocket upgrades do not.
Description
When using Turnstile in front of a Streamlit application, WebSocket connections to
/_stcore/streamfail with:This results in a 502 on the WebSocket endpoint, causing a blank page in Streamlit (which requires WebSocket for its data stream).
Root Cause
internal/httpx/response.godefines a customresponseWriterthat wrapshttp.ResponseWriterbut does not implementhttp.Hijacker. Whenhttputil.ReverseProxy(ininternal/proxy/handler.go) tries to handle a WebSocket upgrade request, it needs to callHijack()on the response writer to take over the TCP connection — but the wrapped writer doesn't support it.Suggested Fix
Add
Hijack()delegation to the custom response writer:Environment
frontend.railway.internal:8501/_stcore/streamLogs
Note: The README states "handles session management, WebSocket and SSE upgrades" — SSE works fine (FlushInterval=-1), but WebSocket upgrades do not.