Update: Go 1.12 added httputil.ReverseProxy support for WebSocket traffic. The answer to this question is now to upgrade your version of Go.
Go's httputil.ReverseProxy doesn't work for WebSocket traffic. The issues with it are detailed here. It looks like the fix/feature may be released in Go 1.12 (see "added this to the Go1.12 milestone" here).
As the fix/feature is not out yet, I am trying to write my own reverse proxy to solve the problem until it is released. On my server, I wrote a simple gorilla/websocket handler that upgrades the connection and then waits for a message from the client.
On my reverse proxy server, I wrote a reverse proxy handler based on the Go 1.12 change submission here, especially the additions in reverseproxy.go. Here is the core of my reverse proxy handler, in relevant part:
outreq := r.WithContext(ctx)
if r.ContentLength == 0 {
outreq.Body = nil
}
p.director(outreq)
outreq.Header = copyHeaders(browserHeaders)
setForwardHeader(r.RemoteAddr, outreq)
log.Infof("outreq URL is %+v", outreq.URL)
transport := http.DefaultTransport
res, err := transport.RoundTrip(outreq)
if err != nil {
log.Errorf("RoundTrip(outreq) failed: %s", err)
}
log.Infof("Server response was %+v", res)
if res.StatusCode != http.StatusSwitchingProtocols {
log.Errorf("Did not receive HTTP status 101 switchingn protocols: %d\n", res.StatusCode)
return
}
if !hasUpgradeHeaders(res.Header) {
log.Error("request does not contain upgrade headers")
return
}
backConn, ok := res.Body.(io.ReadWriteCloser)
if !ok {
log.Errorf("internal error: 101 switching protocols response with non-writable body: %v (%T)", res.Body, res.Body)
return
}
When I try to connect a websocket from my browser, I get as far as that last if statement. The type assertion of the response body to type io.ReadWriteCloser failed. When I log the response I see this:
Server response was &{Status:101 Switching Protocols StatusCode:101 Proto:HTTP/1.1
ProtoMajor:1 ProtoMinor:1 Header:map[Upgrade:[websocket] Connection:[Upgrade]
Sec-Websocket-Accept:[B1fWETPTtNo0bAHFWkFn2lAm9iw=]] Body:{} ContentLength:0
TransferEncoding:[] Close:false Uncompressed:false
Trailer:map[] Request:0xc000112100 TLS:<nil>}
The error log output is this:
ERRO[0012] internal error: 101 switching protocols response with non-writable body: {} (http.noBody)
Why am I getting http.noBody in the response body rather than something implementing the io.ReadWriteCloser interface? How can I resolve the issue so that I can proceed to hijack the connection and facilitate communication between the browser and the server?
Update
I think I found the answer to the "why" question. The latest release of Go does not include this line in persistConn's readLoop function in transport.go in the net/http package:
if resp.isProtocolSwitch() {
resp.Body = newReadWriteCloserBody(pc.br, pc.conn)
}
Compare to current here.
Any ideas for how to resolve?