Repository navigation
Conversation
ff05d69 to
f154711
Compare
Accept's rejection responses had no test that checked the status, headers and body together. Pinning them for every rejection before the 101 lets a change to how the response is written prove that the default output stays the same.
Accept always wrote a plain text http.Error response when it rejected a handshake, so servers with a structured error format had to write a second response, which net/http appends to Accept's body and logs as a superfluous WriteHeader call. ErrorHandler lets the caller write that response instead, with the same signature as gorilla's Upgrader.Error; nil keeps the previous output. Refs #545
f154711 to
0eca8fe
Compare
This comment has been minimized.
This comment has been minimized.
|
Chat: Review posted | View chat Review historydeep-review v0.13.0 | Round 2 | Last posted: Round 2, 14 findings (5 P3, 6 P4, 3 Nit), COMMENT. Review Finding inventoryFinding inventory: coder/websocket PR #588Findings
Contested and acknowledgedCRF-14 (P4, accept.go:91) - Name ErrorHandler implies all Accept errors
Round logRound 1Full panel (round 1). Netero: 1 P3, 1 Note, no P0. Panel: ging-go, ryosuke, kurapika, pariston, mafuuu, bisky, gon, leorio, hisoka, chopper, mafu-san; wildcard meruem (random from distilled set). Kurapika added for the origin rejection path, which put the panel in the 9-12 target. Posted 5 P3, 3 P4, 2 Nit. Gon's comment-wording P2s rated Nit (style, code correct) or dropped. Mafu-san's P2 on rejection table coverage set to P3: both missing branches go through the tested call site at accept.go:136 in unchanged authenticateOrigin; the gap is the default-response pin and the claim. PR description items (CRF-16, CRF-17) have no file location and are posted as a body process observation. Orchestrator verified CRF-3 (1/30 failures of TestAcceptErrorHandler filtered run) and CRF-21 (net/http server.go:1172 excludes 101 from 1xx handling). Reviewed against e1a21e5..0eca8fe. Round 2Fix round: no files added or renamed, no exported signature changed, no function rewritten, +47 effective additions since R1. Churn guard PROCEED: 9 addressed, 1 contested (CRF-14). CRF-11 reopened because 56eccc2 passes the pattern error to ErrorHandler, removing the drop premise. Netero: 1 Note (CRF-26). Panel: gon (resumed R1 agent for CRF-14 dispute check-in), bisky, pariston, meruem, leorio, hisoka, chopper, ryosuke, mafu-san (raised findings the new commits touch), ging-go, kurapika (changed Go files and origin path); wildcard kite. All verified fixes held (CRF-1, 3, 4, 5, 6, 7, 8, 10, 12). CRF-14 closed 1/1. New: 2 P4 (CRF-29, CRF-30), 1 Nit (CRF-27); CRF-11 re-raised and posted at P4 (inaccurate test comment, no runtime effect). Gon's CRF-27 P2 rated Nit (wording of a correct doc). Gon's CRF-28 P4 moved to Note (body): keep-argument is that an unexplained header token leaves readers unsure whether the leak fix depends on it; it stays a note because About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
This PR adds AcceptOptions.ErrorHandler, which writes the HTTP response when Accept rejects a handshake request before the 101 response. 5 P3, 3 P4, 2 Nit.
PR description: "For four rejections, the raw responses match master byte for byte" names neither the four rejections nor the command, so a reviewer cannot repeat it; a real-server comparison of all 12 rejections a server can produce matched base e1a21e5 byte for byte excluding Date, so the sentence can say that. The PR body says Fixes #545 and commit 0eca8fe says Refs #545; use one keyword in both.
Out of scope (needs a ticket or explicit acceptance by a human):
accept.go:175: afterhj.Hijack()fails,Acceptcallshttp.ErrorafterWriteHeader(101). net/http treats 101 as the final status (net/http/server.goexcludesStatusSwitchingProtocolsfrom 1xx handling), so the 500 is a superfluousWriteHeaderand the client receives a 101 with no usable connection.accept.go:143: "http.ResponseWriter does not implement http.Hijacker" does not name the usual causes (an HTTP/2 request, or middleware that wraps theResponseWriterwithoutUnwrap), so an operator has to find the cause without help.accept.go:91: rejection reasons have no stable identity (status 400 covers four reasons), so a handler that maps reasons to RFC 9457typevalues must match error strings. The decision log defers sentinel errors without a ticket.accept.go:207:verifyClientRequestnames the statuserrCode, whileErrorHandlerandwriteRejectionname the same valuestatus.accept.go:385,accept_test.go:566, and other unchanged lines:go fix -diff .reports astrings.SplitSeqrewrite and deadtc := tccopies.
Notes:
accept_test.go:211:newHandshakeRequestbuilds the same request that severalTestAcceptcases build inline; the inline copies predate this PR.accept_test.go:229: in commit 7bd7405, thewantHeadercomment refers to a handler that is added in 0eca8fe.
🤖 This review was automatically generated with Coder Agents.
The doc comment on writeRejection only existed to explain that body is the fallback text for a nil ErrorHandler. The name defaultBody says that at each call site, so the comment is removed.
The ErrorHandler doc did not say whether err is safe to send to the client. For noHijacker, err names a server-side interface, so a handler that writes err.Error() into the response would expose server details.
No test failed if Accept called ErrorHandler after a failed hijack, when the 101 status line is already written. The new case uses a hijacker that returns an error and asserts the handler never runs.
The http.Client transport goroutines in the server subtest exited after TestMain counted goroutines, so filtered runs such as -run TestAccept failed the leak check intermittently. A raw TCP connection with Connection: close leaves no client goroutines behind.
Every test handler discarded the request argument, so passing nil or a copy to ErrorHandler left the tests passing. The table test now asserts the handler gets the same *http.Request that was passed to Accept.
TestAcceptRejectionDefault repeated each row's header and body in a map keyed by row name, and a new row without a map entry failed with a header mismatch that did not name the missing key. The header and body now come from the row, with wantDefaultBody for the one row whose default body is not wantErr.
acceptRejectionTests is documented as covering every rejection, but the Origin parse failure and the Origin without a host had no row, so their responses were not pinned.
The previous wording said err is not meant for the client, but Accept's default response sends err.Error() for every rejection except noHijacker. The doc now leaves the choice of what to send to the handler.
The earlier rewrite dropped the reason for testing over a real connection along with the part that restated the writesNothing row. Only the restated part needed to go.
Accept replaced an invalid OriginPatterns error with "Forbidden", so an ErrorHandler could not tell a misconfigured server from a disallowed origin. The real error now reaches ErrorHandler and Accept's caller, and the default response body stays "Forbidden".
|
/coder-agents-review
|
There was a problem hiding this comment.
Re-review at 56eccc2. 2 P4, 1 Nit, plus one round-1 test comment that 56eccc2 made inaccurate (P4).
The fixes for the round-1 findings hold: the hijack-failure test, the raw-connection server subtest (0 of 40 filtered runs fail, down from 19 of 40), the err doc sentence, the pattern error passed to ErrorHandler, the two Origin rows, the request pointer check, the derived default expectations, and the defaultBody rename. Keeping the name ErrorHandler is accepted.
Notes:
accept.go:135: an invalidOriginPatternsentry is now both written bylog.Printfand returned fromAcceptand passed toErrorHandler, so a server that logsAccepterrors records it twice per cross-origin request. The global log still helps callers that ignore the error.accept_test.go:483: after 56eccc2, thebadOriginPatternrow asserts the new returned error, soTestAcceptRejectionDefaultfails against master'saccept.go. The description's "those match master" is correct for status, headers and body (verified with theerrassertions removed), but running the test against master no longer shows it.accept_test.go:439: theclosetoken inConnection: Upgrade, closehas no comment; removing it changed no result in 5 runs of theserversubtest.
accept.go:273
P4 [CRF-29] Since 56eccc2, failed to parse path pattern "[": syntax error in pattern is the error Accept returns and ErrorHandler receives, and it does not say the pattern is an OriginPatterns entry. (Leorio)
Before 56eccc2 only log.Printf printed this text. An operator who logs Accept errors now sees failed to accept WebSocket connection: failed to parse path pattern "["... (pinned in TestAcceptRejectionDefault), and "path pattern" points them at URL routes instead of the option. Change the message to fmt.Errorf("invalid OriginPatterns entry %q: %w", hostPattern, err) and update the badOriginPattern row.
🤖
🤖 This review was automatically generated with Coder Agents.
| wantHeader: http.Header{}, | ||
| }, | ||
| { | ||
| // The pattern error is logged and not sent to the client. |
There was a problem hiding this comment.
P4 [CRF-11] "The pattern error is logged and not sent to the client." is no longer true for the handler path this row drives. (Gon)
Since 56eccc2 the row sets wantErr to the pattern error and wantErrIs: path.ErrBadPattern, and TestAcceptErrorHandler asserts that ErrorHandler receives it, so a handler that writes err sends the pattern to the client. Only the nil-handler body hides it, which wantDefaultBody: "Forbidden" already shows. Change it to // The default body hides the pattern error, which Accept logs.
🤖
| // ErrorHandler writes the HTTP response when Accept rejects the handshake | ||
| // request. status is the HTTP status Accept chose, w.Header() holds the | ||
| // headers Accept set for the rejection, and Accept returns an error | ||
| // wrapping err. err may contain server details, so ErrorHandler decides |
There was a problem hiding this comment.
Nit [CRF-27] "so ErrorHandler decides what to send to the client" repeats the doc's first sentence, which already says ErrorHandler writes the response. (Gon)
The fact the caller needs is that err may contain server details; err may contain server details. says it.
🤖
| // is a response to a ping and does not trigger any further frame transmission. | ||
| OnPongReceived func(ctx context.Context, payload []byte) | ||
|
|
||
| // ErrorHandler writes the HTTP response when Accept rejects the handshake |
There was a problem hiding this comment.
P4 [CRF-30] The ErrorHandler doc does not say how a handler can tell an invalid OriginPatterns entry from a disallowed origin, though 56eccc2's commit message gives that as the reason for the change. (Leorio)
Both cases arrive with status 403, so a handler can separate them only with errors.Is(err, path.ErrBadPattern) or by matching error text. The badOriginPattern row pins wantErrIs: path.ErrBadPattern, so the tests already treat the wrap as a contract. Add "For an invalid OriginPatterns entry, err wraps path.ErrBadPattern." to the doc, or drop the wantErrIs assertion if the wrap is not meant to be relied on.
🤖
Acceptwrites a plain texthttp.Errorresponse when it rejects a handshake request. A server that needs a structured error body, such as RFC 9457 JSON (#545), cannot replace that response. A second write afterAcceptreturns only appends to the plain text body: the status andtext/plaincontent type stayAccept's, and net/http logs a superfluousWriteHeadercall.AcceptOptions.ErrorHandler func(w http.ResponseWriter, r *http.Request, status int, err error)writes that response instead. It receives the statusAcceptchose and the errorAcceptreturns, before wrapping.w.Header()already holds the headersAcceptset, such asSec-WebSocket-Version: 13for an unsupported version. It is called for every rejection before the 101 response: request validation, origin, and aResponseWriterwithouthttp.Hijacker. It is not called when hijacking fails, becauseAccepthas already written 101 Switching Protocols. A handler that writes nothing leaves net/http's default 200 OK with an empty body.A nil
ErrorHandlerkeeps the previous response. For an invalidOriginPatternsentry,Acceptnow returns the pattern error instead of "Forbidden", and the handler receives it. The client still gets 403 Forbidden. TheAcceptdoc now says not to write towafterAcceptreturns an error.Tests cover the handler for all 13 rejections. A separate test pins the default status, headers and body for all 13, and those match master.
Fixes #545
Decision log
r, so one handler shared across requests can use it, for exampler.Context(). Allow customize how HTTP errors are rendered whenwebsocket.Acceptfails to upgrade #545 discussed capturing the request in a closure instead, which does not work for shared options.websocket.Acceptfails to upgrade #545, Support disabling error writing via AcceptOptions #152).OriginPatternsentry and withouthttp.Hijacker, the handler receives the descriptive error while the nil default body stays "Forbidden" or "Not Implemented". TheErrorHandlerdoc sayserrmay contain server details.recoveraround the handler: a panic behaves like any other handler panic in net/http.AcceptOptionsstub so shared code compiles.