Skip to content

[Client] Send HTTP requests from outside the request fiber - #599

Open
aton-of-data wants to merge 1 commit into
modelcontextprotocol:mainfrom
aton-of-data:client-send-outside-request-fiber
Open

aton-of-data wants to merge 1 commit into
modelcontextprotocol:mainfrom
aton-of-data:client-send-outside-request-fiber

Conversation

@aton-of-data

Copy link
Copy Markdown
Contributor

Fixes #504

Reproduction and mechanism: #504 (comment). Lines are on main at d1e8091.

Why it fails

connect() runs the handshake in a fiber (HttpTransport.php:141-148), and send() calls sendRequest() inside it (line 211). An amphp-based client does a bare Fiber::suspend() there and waits for the Revolt loop, which only runs from {main}. So the request never leaves the process. processFiber() then resumes it with the timeout Error (line 1027), and Amp throws the FutureIterator::consume() TypeError. Body reads (line 261) suspend the same way.

Change

  • Inside the request fiber, send() suspends with the message.
  • tick() first calls processSends(), which sends it from {main} and resumes the fiber. A send failure is thrown back into the fiber.
  • processFiber() skips a fiber that waits for a send.
  • connect() keeps what start() returns, as runRequest() does.
  • checkInterruption() keeps what throw() returns: the notifications/cancelled sent since [Client] Throw TimeoutException on request timeout and cancel the request #567 now steps out too.

The errors from #577 are stored during the send, so the fiber still finds them as an immediate response. Every client takes this path, so the existing transport tests cover it too.

Verification

PHP 8.3.6, composer update --prefer-stable.

  • New test: its fake client suspends like Revolt in sendRequest() and body reads. On main it fails with HTTP request failed: Resumed by something other than the event loop, with Mcp\Schema\JsonRpc\Error. With the change it passes.
  • Unit: 2558 tests (4 skipped) on main, 2559 (4 skipped) with the change, all green.
  • phpstan and php-cs-fixer fix --dry-run: clean.
  • Integration: 108 tests, the same 8 DualEraElicitationTest errors on main and with the change. That example prints its shebang under php -S here.
  • Interop: 175 tests, the same 44 Interop\Server\Http errors on main and with the change. All 72 Interop\Client tests pass (1 skipped).
  • End to end with amphp's PSR-18 adapter (http-client 5.3.6) against the discovery-calculator example: main fails after 3.00s with the TypeError, the change connects and lists tools. Curl, native and discovery still connect.

Not verified

  • Symfony's Psr18Client(new AmpHttpClient()) on this base (needs PHP 8.4).
  • ReactPHP clients, listen: true with Amp.
  • Conformance suites (Docker), ci-lowest, PHP other than 8.3.

An amphp-backed PSR-18 client (Symfony's AmpHttpClient, or amphp's own
adapter) suspends the fiber it is called from and waits for the Revolt
event loop to resume it. HttpTransport::send() ran inside the request
fiber, so nothing drove that loop: the request never left the process,
and processFiber() eventually resumed Amp's suspension with the timeout
Error, which surfaced as "FutureIterator::consume(): Return value must
be of type ?array, Mcp\Schema\JsonRpc\Error returned".

Inside the request fiber, send() now suspends with the message and the
transport's tick sends it from {main}, where Revolt runs its loop
itself. Response body reads happen there too, since they suspend the
same way. A failed send is thrown back into the fiber, so it still
surfaces from send(), and the cancellation notification sent after a
timeout or a cancellation goes out the same way.

Fixes modelcontextprotocol#504

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Interruption handling can enter a blocking SSE read before sending the yielded cancellation notification.

1 open finding
What changed in this PR

Moves HTTP I/O outside SDK-managed request fibers to support Amp-based PSR-18 clients.

Changes:

  • Defers fiber-originated sends to the transport loop.
  • Preserves suspension state during initialization and cancellation.
  • Adds Amp-like fiber suspension coverage and changelog documentation.
File Description
src/​Client/​Transport/​HttpTransport.php Processes HTTP sends outside request fibers.
tests/​Unit/​Client/​Transport/​HttpTransportTest.php Tests fiber-suspending HTTP clients and response bodies.
CHANGELOG.md Documents the compatibility fix.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +613 to +614
// The fiber may step out again to send the cancellation notification.
$this->activeSuspend = $this->activeFiber->throw($error);

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.

[Client] HttpTransport resumes an Amp-suspended Fiber with a JSON-RPC error

2 participants