This issue has been rewritten. It originally claimed that close() discarding in-flight requests was an SDK defect. Implementing that broke test-e2e on Node 20/22/24 against your own conformance requirement hosting:session:delete-cancels-inflight, which is right and my claim was not. The withdrawn claim and what survives it are both below; nothing has been deleted, only corrected.
What I hit
A client hung for its full idle timeout on a tools/call while the session it was running under was torn down. The HTTP response it received was 200 text/event-stream, chunked, zero bytes — complete and successful at the transport layer, with no JSON-RPC answer for the id it was waiting on.
Against a server whose session is torn down mid-call (DELETE), no proxy involved:
|
HTTP |
Content-Type |
Transfer-Encoding |
bytes |
answer for the request id? |
| baseline |
200 |
text/event-stream |
— |
941 498 |
yes |
| torn down, trial 1 |
200 |
text/event-stream |
chunked |
0 |
no |
| trial 2 |
200 |
text/event-stream |
chunked |
0 |
no |
| trial 3 |
200 |
text/event-stream |
chunked |
0 |
no |
7/7 across two runs, deterministic, on 1.16.0; the same code shape is on main at 3924de9.
Why that is not the bug I thought it was
WebStandardStreamableHTTPServerTransport.close() ends every per-request SSE stream without writing an event. I read that as a lost reply. It isn't:
hosting:session:delete-cancels-inflight — DELETE on a session aborts every in-flight request handler's RequestHandlerExtra.signal; their POST-initiated SSE streams close without a JSON-RPC response being written.
The handler is aborted, so the request is cancelled, and a cancelled JSON-RPC request gets no response. An empty stream is the encoding of cancellation. My reproduction forces teardown with an explicit DELETE, which is exactly the input that requirement covers — so the table above is reproducing intended behaviour, and the "expected" section this issue used to carry is withdrawn.
What survives, and where it went
The JSON-response-mode path admits no such reading. handleRequest() returns a Promise<Response> that only send() resolves; a stream mapping's cleanup deletes the entry without settling it, so close() during an in-flight POST leaves the HTTP request open until the socket dies. There is no cancellation semantics for "the response never arrives and the socket stays open".
That is already tracked as #2559, and #2692 fixes exactly it — packages/server 478 passing, test/e2e 2639 passing with hosting:session:delete-cancels-inflight intact.
The question that is actually left
In my case the teardown came from an intermediary, not from the caller that was waiting. No client cancelled anything; a proxy between them terminated the session, and the waiting caller could not distinguish that from a slow call.
handleDeleteRequest does not call close() — the hosting layer does — so the transport cannot tell "this client terminated its session" from "the process is shutting down", and resolving that ambiguity as cancelled is a reasonable default. The open question is whether the hosting layer should be able to signal in-flight callers when a session teardown was not requested by the caller waiting on it, or whether that is squarely the intermediary's problem to solve (which is what I ended up doing downstream).
If maintainers don't want to carry that as an open question, please close this as invalid — #2692 stands on #2559 alone and does not depend on it.
What I hit
A client hung for its full idle timeout on a
tools/callwhile the session it was running under was torn down. The HTTP response it received was200 text/event-stream, chunked, zero bytes — complete and successful at the transport layer, with no JSON-RPC answer for the id it was waiting on.Against a server whose session is torn down mid-call (
DELETE), no proxy involved:text/event-streamtext/event-streamtext/event-streamtext/event-stream7/7 across two runs, deterministic, on
1.16.0; the same code shape is onmainat3924de9.Why that is not the bug I thought it was
WebStandardStreamableHTTPServerTransport.close()ends every per-request SSE stream without writing an event. I read that as a lost reply. It isn't:The handler is aborted, so the request is cancelled, and a cancelled JSON-RPC request gets no response. An empty stream is the encoding of cancellation. My reproduction forces teardown with an explicit
DELETE, which is exactly the input that requirement covers — so the table above is reproducing intended behaviour, and the "expected" section this issue used to carry is withdrawn.What survives, and where it went
The JSON-response-mode path admits no such reading.
handleRequest()returns aPromise<Response>that onlysend()resolves; a stream mapping'scleanupdeletes the entry without settling it, soclose()during an in-flight POST leaves the HTTP request open until the socket dies. There is no cancellation semantics for "the response never arrives and the socket stays open".That is already tracked as #2559, and #2692 fixes exactly it —
packages/server478 passing,test/e2e2639 passing withhosting:session:delete-cancels-inflightintact.The question that is actually left
In my case the teardown came from an intermediary, not from the caller that was waiting. No client cancelled anything; a proxy between them terminated the session, and the waiting caller could not distinguish that from a slow call.
handleDeleteRequestdoes not callclose()— the hosting layer does — so the transport cannot tell "this client terminated its session" from "the process is shutting down", and resolving that ambiguity as cancelled is a reasonable default. The open question is whether the hosting layer should be able to signal in-flight callers when a session teardown was not requested by the caller waiting on it, or whether that is squarely the intermediary's problem to solve (which is what I ended up doing downstream).If maintainers don't want to carry that as an open question, please close this as invalid — #2692 stands on #2559 alone and does not depend on it.