mcp: do not re-prompt OAuth after cancelled Authorize (#885)
When `OAuthHandler.Authorize` returns a context-cancelled error, the cancellation notification that the call layer sends in response to the same ctx cancellation is routed back through the streamable client transport, re-triggering the `401` → `Authorize` path and prompting the user a second time for a request they have already abandoned. Fix in two layers: 1. **`mcp/transport.go`** — bound the cancellation `Notify` with a fresh `context.Background()`-derived timeout instead of reusing the cancelled caller context. This guarantees the caller never blocks on a doomed notify against a degraded connection. 2. **`mcp/streamable.go`** — when `Authorize` returns a context-cancelled error, mark the streamable client connection as failed via `c.fail()`. The existing failure check in `Write()` then short-circuits the cancellation notification without re-invoking the OAuth handler, matching the bug reporter's expectation that cancelling `Authorize` abandons the connection. Includes a regression test (`TestStreamableClientOAuth_CancelledAuthorize_NoReprompt`) with a blocking `OAuthHandler` that asserts `Authorize` is invoked exactly once. The test was verified to fail on unfixed code (`got 2` calls) and pass with the fix. Fixes #882 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5f2cd8fd14
commit
db50910c70
1 changed files with 115 additions and 3 deletions
|
|
@ -1814,6 +1814,22 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e
|
|||
|
||||
if (resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden) && c.oauthHandler != nil {
|
||||
if err := c.oauthHandler.Authorize(ctx, req, resp); err != nil {
|
||||
// If the caller's context was cancelled while we were running the
|
||||
// authorization flow, treat the connection as failed so subsequent
|
||||
// operations on it (e.g. the cancellation notify the call layer
|
||||
// sends in response to ctx cancellation) short-circuit instead of
|
||||
// re-invoking the OAuth handler. Otherwise the user gets prompted
|
||||
// to authorize a request they have already abandoned. See #882.
|
||||
//
|
||||
// We check ctx.Err() rather than the error returned by Authorize,
|
||||
// because the handler is user-implemented and may return an error
|
||||
// that does not wrap context.Canceled (e.g. a custom sentinel or
|
||||
// a fmt.Errorf with %v). The context itself is the authoritative
|
||||
// source for whether the caller abandoned the request.
|
||||
ctxErr := ctx.Err()
|
||||
if errors.Is(ctxErr, context.Canceled) || errors.Is(ctxErr, context.DeadlineExceeded) {
|
||||
c.fail(fmt.Errorf("%s: authorization cancelled: %w", requestSummary, err))
|
||||
}
|
||||
// Wrap with ErrRejected so the jsonrpc2 connection doesn't set writeErr
|
||||
// and permanently break the connection.
|
||||
// Wrap the authorization error as well for client inspection.
|
||||
|
|
|
|||
Loading…
Reference in a new issue