Skip to content

feat: add ErrBadStatus for non-101 Dial responses - #587

Draft
mafredri wants to merge 2 commits into
masterfrom
mafredri/dial-err-bad-status
Draft

mafredri wants to merge 2 commits into
masterfrom
mafredri/dial-err-bad-status

Conversation

@mafredri

Copy link
Copy Markdown
Member

Dial now wraps a new sentinel error, ErrBadStatus, when the server answers the opening handshake with a status other than 101 Switching Protocols. After errors.Is(err, websocket.ErrBadStatus), the returned response is non-nil and resp.Body holds up to the first 1024 bytes of the response body.

Before this change, the only signal was a non-nil response, as advised in #424. But Dial also returns a non-nil response when a 101 response fails validation: an invalid Connection, Upgrade or Sec-WebSocket-Accept header, an unexpected subprotocol or extension, or a body that is not writable. A caller that treats every non-nil response as an HTTP rejection reports those protocol failures as rejections. ErrBadStatus is not wrapped in those cases, or when no response arrives.

The error text is unchanged on purpose, for example failed to WebSocket dial: expected handshake response status code 101 but got 403, because public projects match on it. An unexported error type returns that text and unwraps to ErrBadStatus.

On js/wasm, Dial never wraps ErrBadStatus, because the browser does not expose the handshake response.

New tests cover a 403 with a 1025-byte body, a 101 with an invalid Sec-WebSocket-Accept, and a refused connection. The 403 test fails on master with only the variable declared. Lint, race tests and the WASM test pass.

Refs #424, #528

Decision log
  • Name and meaning follow ErrBadStatus in golang.org/x/net/websocket.
  • Only a non-101 status wraps it; invalid 101 headers and a non-writable body keep their own errors, as in x/net/websocket.
  • Unwrap instead of an Is method, so ErrBadStatus is in the error chain that the docs describe as wrapped.
  • Rejected: a %w prefix (websocket: bad status: expected ...), because it changes text that public projects match on.
  • Rejected: an exported struct with status, header and body, because it duplicates the returned response.
  • Rejected: an error that holds the *http.Response, because its body is the same reader as the returned response's body, so one read empties both.
  • Rejected: documenting the resp.StatusCode != 101 check only, because it gives no condition that errors.Is can test after a caller wraps the error.
  • Unchanged: the 1024-byte limit, and Dial still closes the body.
  • Separate change: skipping the body read after a 101 response.

🤖 This PR was created with the help of Coder Agents.

Dial returned a non-nil response both when the server rejected the
handshake with a non-101 status and when a 101 response failed
validation, so callers checking resp != nil treated protocol violations
as HTTP errors. Dial now wraps ErrBadStatus only for a non-101 status,
and documents that the response and its first 1024 body bytes are
available in that case.
Wrapping ErrBadStatus with %w inserted "websocket: bad status: " into
the Dial error string, and public projects match on the existing text.
An unexported error type now returns the previous text and unwraps to
ErrBadStatus, so errors.Is still holds.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant