Fix tests timing out during interactive OAuth flow (#2510) - #2544
Fix tests timing out during interactive OAuth flow (#2510)#2544GhagSagar23 wants to merge 6 commits into
Conversation
|
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
Fixes modelcontextprotocol#2510 by serializing auth requests in StreamableHTTPClientTransport and properly handling 401s without crashing the test runner.
615c4b9 to
a740d28
Compare
gnanirahulnutakki
left a comment
There was a problem hiding this comment.
Reviewed exact head a740d28ca71f6bc4221f63ad01fd9faac0ea15d3 against #2510's redirect-completion requirement. The client package passes typecheck, lint, and all 771 tests locally, but the current tests still encode the pre-fix rejection path. I found one lifecycle issue that leaves the reported mid-session request dead-ended.
| stepUpRetries = 0 | ||
| ): Promise<void> { | ||
| if (this._pendingAuthPromise) { | ||
| await this._pendingAuthPromise; |
There was a problem hiding this comment.
[P1] Keep the triggering request pending across REDIRECT
This await only observes a promise that finishAuth() creates later. In #2510's mid-session path, _send() is already inside onUnauthorized; handleOAuthUnauthorized() receives REDIRECT and throws UnauthorizedError, so the original send() rejects before the browser callback can call finishAuth() or create this promise. On this exact head, the isolated existing test uses custom fetch during auth flow on 401 - no global fetch fallback still passes specifically by expecting that rejection, and this PR changes no tests. Thus the code may exchange a callback code later, but it still cannot resume and retry the triggering request. Please establish the deferred when REDIRECT is produced, leave the triggering request pending, resolve/reject it from finishAuth()/close(), retry the request, and cover that full sequence.
… completes - Updates `StreamableHTTPClientTransport` to intercept `UnauthorizedError` during requests. - Retains resolve/reject handlers in a `_pendingAuthPromise` to prevent premature request failure and manual resends. - Retries the pending request upon successful `finishAuth()` completion. - Updates tests in `streamableHttp.test.ts` to handle the new pending promise behavior instead of immediate rejection. - Fixes Vitest mock leaking between test suites in `streamableHttp.test.ts` by using `mockReset` instead of `mockRestore`. - Updates `probeAuthSeam.test.ts` to align with the new `finishAuth` contract where probe requests remain pending until resolved.
|
I've updated the implementation to address this feedback! Triggering requests now correctly remain pending in the background until Changes made:
All 798 tests across the client packages are green. Let me know if there's anything else you'd like me to address! |
* StreamableHTTPClientTransport._send and _startOrAuthSse now wait for finishAuth() rather than rejecting immediately with UnauthorizedError when a REDIRECT occurs. * Refactored OAuth test suite to support asynchronous redirect flows. Tests now hang gracefully during REDIRECT, waiting for finishAuth() rather than asserting immediate rejections. * Updated simpleOAuthClient example to accurately reflect the new asynchronous flow. * Fixed sporadic vitest timeouts and FakeTimers leakage by properly managing unhandled transport promises during tests.
gnanirahulnutakki
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 4dbb8a0328af0f1e700daf0e235a75d0c632b585. The happy path is improved: 401 -> REDIRECT now creates a deferred, successful finishAuth() releases it, and the original POST retries; the two focused client files pass 81/81 on Node 24.
The original P1 remains on the callback-failure path. finishAuth() awaits resolveAuthorizationCallbackParams(...) at streamableHttp.ts:918-924 before the try/catch that rejects the shared deferred at L935-L957. An error-shaped callback therefore rejects finishAuth() but leaves the triggering request pending. Exact-head repro output was finishAuth=OAuthError: user denied, then after_finish=pending, deferred_present=true; only close() changed it to rejected: Transport closed. Please put callback resolution under the same settlement boundary and add a regression asserting the original request rejects with the callback error without requiring close().
The five red required checks are also branch-caused relative to all-green base cc4b4161: Prettier rejects the three changed client files; the scoped-tools example still waits for the first client.connect() to reject at client.ts:55-67, so CI hangs and the local exact-head run timed out; and Node 20/22/24 E2E each report the same seven failures because two timeout requirements gained transports without the required note, while known SSE progress failures were removed without an SSE implementation fix. Node 24 reproduces all six SSE failures plus the manifest failure.
Please also remove the four unconditional console.log("CATCH BLOCK ERROR:", error) statements added at streamableHttp.ts:384,446,593,1109; these log authentication errors from library code and contribute to the formatter failure. The original review thread remains unresolved until the failure path settles the shared request.
Summary
This PR fixes the test timeouts and potential hanging requests that occur when an interactive OAuth flow (returning
REDIRECT) is triggered by theStreamableHTTPClientTransport. It addresses issue #2510.Changes
StreamableHTTPClientTransport._sendandStreamableHTTPClientTransport._startOrAuthSseto explicitly await the_pendingAuthPromisewhen theauth()function returnsREDIRECT._handleAuthResultmethod to serialize requests waiting on the OAuth redirect flow and to throwUnauthorizedErrorwhen the result is notAUTHORIZED.finishAuthproperly resolves or rejects the pending auth promise.streamableHttp.test.tsto mockredirectToAuthorizationby throwing anUnauthorizedError, which properly simulates the rejection path for the tests without timing out.