An upgrade the app accepts under another protocol name is answered 502 (closes #131)
check / check (push) Canceled after 0s

ReverseProxy refuses a 101 whose Upgrade is not the one the client asked
for, but modifyResponse had already counted the 101 as sent, so
answerError sent nothing and the client got an empty 200. The 101 now
counts as sent only once ReverseProxy has taken over the client's
connection, and answerError closes the app's switched connection, which
ReverseProxy leaves open when it refuses the switch.

Model: opus-5-5
This commit was merged in pull request #147.
This commit is contained in:
2026-10-20 15:36:30 +02:00
parent b0d54a2c51
commit 75f28ee0b5
3 changed files with 42 additions and 3 deletions
+15 -2
View File
@@ -1,8 +1,10 @@
package proxy package proxy
import ( import (
"bufio"
"errors" "errors"
"io" "io"
"net"
"net/http" "net/http"
"sync/atomic" "sync/atomic"
@@ -211,9 +213,20 @@ func (w *responseWriter) FlushError() error {
return err return err
} }
// Hijack takes over the connection to the client, which ReverseProxy does
// only to pass on the app's 101 Switching Protocols: that is then the
// status sent.
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
conn, buffered, err := http.NewResponseController(w.ResponseWriter).Hijack()
if err == nil {
w.status = http.StatusSwitchingProtocols
}
return conn, buffered, err
}
// Unwrap lets http.ResponseController reach net/http's own // Unwrap lets http.ResponseController reach net/http's own
// ResponseWriter, which is how ReverseProxy takes over the connection of // ResponseWriter for what responseWriter does not do itself.
// an upgraded request.
func (w *responseWriter) Unwrap() http.ResponseWriter { func (w *responseWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter return w.ResponseWriter
} }
+17
View File
@@ -309,6 +309,23 @@ func echoAfterUpgrade(w http.ResponseWriter, r *http.Request) {
} }
} }
func TestAnswers502WhenTheAppSwitchesToAnotherProtocol(t *testing.T) {
t.Parallel()
// answerAfterUpgrade switches to websocket whatever the client asked
// for, and ReverseProxy passes on a switch only to the protocol the
// client asked for, here "websocket, foo".
app := startApp(t, answerAfterUpgrade)
addr, out := startProxy(t, app.URL, nil)
conn := dial(t, addr)
send(t, conn, "GET /ws HTTP/1.1\r\nHost: app\r\n"+
"Connection: Upgrade\r\nUpgrade: websocket, foo\r\n\r\n")
wantStatus(t, readResponse(t, conn), http.StatusBadGateway)
wantLine(t, out.requestLine(t), http.StatusBadGateway, requestlog.ActionUpstreamError)
}
func TestServerHasTheDefaultLimits(t *testing.T) { func TestServerHasTheDefaultLimits(t *testing.T) {
t.Parallel() t.Parallel()
+10 -1
View File
@@ -395,8 +395,10 @@ func (rq *request) modifyResponse(res *http.Response) error {
// timeouts. ReverseProxy writes this answer straight to the // timeouts. ReverseProxy writes this answer straight to the
// connection it takes over, not through rq.out, and then copies // connection it takes over, not through rq.out, and then copies
// what passes each way through res.Body, the connection to the app. // what passes each way through res.Body, the connection to the app.
// It may still refuse the switch, as when the app's Upgrade is not
// the client's, so the status is noted only once it has taken over
// the connection, by rq.out's Hijack.
rq.stopTimers() rq.stopTimers()
rq.out.status = res.StatusCode
rq.line.Websocket = true rq.line.Websocket = true
conn, ok := res.Body.(io.ReadWriteCloser) conn, ok := res.Body.(io.ReadWriteCloser)
@@ -428,6 +430,13 @@ func (rq *request) modifyResponse(res *http.Response) error {
// answerError is ReverseProxy's ErrorHandler: the request could not be // answerError is ReverseProxy's ErrorHandler: the request could not be
// passed to the app, or the app's answer cannot be passed on. // passed to the app, or the app's answer cannot be passed on.
func (rq *request) answerError(_ http.ResponseWriter, _ *http.Request, err error) { func (rq *request) answerError(_ http.ResponseWriter, _ *http.Request, err error) {
if rq.upgraded != nil {
// The app switched protocols, but the switch was not passed on,
// and ReverseProxy leaves the app's connection open when it
// refuses it.
_ = rq.upgraded.Close()
}
refused := rq.refused.Load() refused := rq.refused.Load()
if refused == nil { if refused == nil {
if rq.in.Context().Err() != nil { if rq.in.Context().Err() != nil {