Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,10 @@ func (opts *DialOptions) cloneWithDefaults(ctx context.Context) (context.Context
// The response is the WebSocket handshake response from the server.
// You never need to close resp.Body yourself.
//
// If an error occurs, the returned response may be non nil.
// However, you can only read the first 1024 bytes of the body.
// If an error occurs, the returned response may be non nil. After a 101
// response, resp.Body is empty, because its bytes belong to the upgraded
// connection. For any other status, resp.Body holds at most the first
// 1024 bytes of the body.
//
// This function requires at least Go 1.12 as it uses a new feature
// in net/http to perform WebSocket handshakes.
Expand Down Expand Up @@ -148,6 +150,15 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) (
resp.Body = nil
defer func() {
if err != nil {
if resp.StatusCode == http.StatusSwitchingProtocols {
// The body is the upgraded connection, not an HTTP body, and
// reading it stalls until the timer below fires unless the
// server closes it or sends 1024 bytes.
respBody.Close()
resp.Body = http.NoBody
return
}

// We read a bit of the body for easier debugging.
r := io.LimitReader(respBody, 1024)

Expand Down
62 changes: 62 additions & 0 deletions dial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,65 @@ func TestDialViaProxy(t *testing.T) {
assertEcho(t, ctx, c)
assertClose(t, c)
}

func TestDialFailedHandshakeBody(t *testing.T) {
t.Parallel()

t.Run("switchingProtocols", func(t *testing.T) {
t.Parallel()

s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, brw, err := http.NewResponseController(w).Hijack()
if err != nil {
t.Errorf("failed to hijack: %v", err)
return
}
defer conn.Close()
brw.WriteString("HTTP/1.1 101 Switching Protocols\r\n" +
"Connection: Upgrade\r\n" +
"Upgrade: websocket\r\n" +
"Sec-WebSocket-Accept: xd\r\n\r\n" +
"after upgrade")
brw.Flush()
// Keep the connection open, so reading it would stall.
io.Copy(io.Discard, conn)
}))
defer s.Close()

ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()

start := time.Now()
_, resp, err := websocket.Dial(ctx, s.URL, nil)
elapsed := time.Since(start)
assert.Contains(t, err, "invalid Sec-WebSocket-Accept")
assert.Equal(t, "StatusCode", http.StatusSwitchingProtocols, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
assert.Success(t, err)
assert.Equal(t, "body", "", string(b))
// Reading the body would take the 3s of Dial's read timer, and 1s
// leaves margin for slow CI.
if elapsed > time.Second {
Comment thread
mafredri marked this conversation as resolved.
Resolved
t.Fatalf("Dial took %v, want under 1s", elapsed)
}
})

t.Run("forbidden", func(t *testing.T) {
t.Parallel()

s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "forbidden by test", http.StatusForbidden)
}))
defer s.Close()

ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()

_, resp, err := websocket.Dial(ctx, s.URL, nil)
assert.Error(t, err)
assert.Equal(t, "StatusCode", http.StatusForbidden, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
assert.Success(t, err)
assert.Equal(t, "body", "forbidden by test\n", string(b))
})
}
Loading