Skip to content

Commit 5fc6504

Browse files
nabacoclaude
andcommitted
fix(janus-proxy): tunnel raw bytes to preserve WS upgrade headers
The previous proxy used http.ReadResponse + resp.Write to forward Janus's 101 Switching Protocols response back to the browser. Both steps mangled the upgrade: - ReadResponse normalizes header casing via MIME canonicalization, so Janus's "Sec-Websocket-Accept" / "Upgrade: WebSocket" headers came out rewritten and the browser closed the connection. - resp.Write treats a 101 response's Body as the rest of the upgraded stream and tries to copy it into the client conn before we ever get to the bidirectional bridge — blocking client→server frames. Read the response status + headers as raw bytes until \r\n\r\n, verify HTTP/1.1 101, write the bytes verbatim to the client, then start the two-way io.Copy. Browser-side WebSocket now establishes cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1d18d74 commit 5fc6504

1 file changed

Lines changed: 28 additions & 8 deletions

File tree

internal/api/handlers_janus.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package api
22

33
import (
44
"bufio"
5+
"bytes"
56
"fmt"
67
"io"
78
"net"
@@ -65,18 +66,37 @@ func (s *Server) handleJanusWS(w http.ResponseWriter, r *http.Request) {
6566
defer backConn.Close()
6667

6768
// Forward the WebSocket upgrade request to Janus verbatim.
68-
fmt.Fprintf(backConn, "GET /janus HTTP/1.1\r\nHost: %s\r\n", s.cfg.Janus.WSAddr)
69+
var req bytes.Buffer
70+
fmt.Fprintf(&req, "GET /janus HTTP/1.1\r\nHost: %s\r\n", s.cfg.Janus.WSAddr)
6971
for k, vv := range r.Header {
7072
for _, v := range vv {
71-
fmt.Fprintf(backConn, "%s: %s\r\n", k, v)
73+
fmt.Fprintf(&req, "%s: %s\r\n", k, v)
7274
}
7375
}
74-
fmt.Fprintf(backConn, "\r\n")
76+
fmt.Fprintf(&req, "\r\n")
77+
if _, err := backConn.Write(req.Bytes()); err != nil {
78+
http.Error(w, "janus write failed", http.StatusBadGateway)
79+
return
80+
}
7581

82+
// Read the response status line + headers as raw bytes (no http.ReadResponse —
83+
// it canonicalizes header casing and resp.Write tries to consume a 101 body,
84+
// both of which break the WebSocket upgrade).
7685
br := bufio.NewReader(backConn)
77-
resp, err := http.ReadResponse(br, nil)
78-
if err != nil || resp.StatusCode != http.StatusSwitchingProtocols {
79-
http.Error(w, "janus handshake failed", http.StatusBadGateway)
86+
var respHead bytes.Buffer
87+
for {
88+
line, err := br.ReadBytes('\n')
89+
if err != nil {
90+
http.Error(w, "janus read failed", http.StatusBadGateway)
91+
return
92+
}
93+
respHead.Write(line)
94+
if bytes.Equal(line, []byte("\r\n")) {
95+
break
96+
}
97+
}
98+
if !bytes.HasPrefix(respHead.Bytes(), []byte("HTTP/1.1 101")) {
99+
http.Error(w, "janus did not upgrade", http.StatusBadGateway)
80100
return
81101
}
82102

@@ -91,8 +111,8 @@ func (s *Server) handleJanusWS(w http.ResponseWriter, r *http.Request) {
91111
}
92112
defer clientConn.Close()
93113

94-
// Send the 101 Switching Protocols response to the browser.
95-
if err := resp.Write(clientConn); err != nil {
114+
// Forward the 101 response verbatim.
115+
if _, err := clientConn.Write(respHead.Bytes()); err != nil {
96116
return
97117
}
98118

0 commit comments

Comments
 (0)