Delay SSE GET connection until after session ID is established (#97)

* Delay SSE GET connection until after session ID is established

- Add a signaling mechanism to ensure the SSE streaming task waits for the initial session ID to be set before attempting the GET connection.
- Use a TaskGroup to race between session ID signal and a 10-second timeout, logging the outcome.
- Trigger the signal when a session ID is received for the first time from any response.
- Add detailed logging for all code paths to aid debugging and clarify connection timing.
- Clean up signal resources on disconnect to prevent leaks.

This addresses the initialization timing issue where the SSE GET could be attempted before the session was established, as discussed in MCP PR #91.

* Update tests for SSE timing

* Use configurable timeout for acquiring session ID

* Remove Actor protocol conformance for HTTPClientTransport

* Configure shorter session ID timeout for tests

* Add documentation comments to HTTPClientTransport

* Rename sessionIDWaitTimeout to sseInitializationTimeout
This commit is contained in:
Mattt 2025-05-06 06:49:48 -07:00 • committed by GitHub
commit bdaa2585c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -415,24 +415,45 @@ import Testing
endpoint: testEndpoint,
configuration: configuration,
streaming: true,
sseInitializationTimeout: 1,
logger: nil
)
let eventString = "id: event1\ndata: {\"key\":\"value\"}\n\n"
let sseEventData = eventString.data(using: .utf8)!
// First, set up a handler for the initial POST that will provide a session ID
await MockURLProtocol.requestHandlerStorage.setHandler {
[testEndpoint] (request: URLRequest) in
let response = HTTPURLResponse(
url: testEndpoint, statusCode: 200, httpVersion: "HTTP/1.1",
headerFields: [
"Content-Type": "text/plain",
"Mcp-Session-Id": "test-session-123",
])!
return (response, Data())
}
// Connect and send a dummy message to get the session ID
try await transport.connect()
try await transport.send(Data())
// Now set up the handler for the SSE GET request
await MockURLProtocol.requestHandlerStorage.setHandler {
[testEndpoint, sseEventData] (request: URLRequest) in // sseEventData is now empty Data()
#expect(request.url == testEndpoint)
#expect(request.httpMethod == "GET")
#expect(request.value(forHTTPHeaderField: "Accept") == "text/event-stream")
#expect(
request.value(forHTTPHeaderField: "Mcp-Session-Id") == "test-session-123")
let response = HTTPURLResponse(
url: testEndpoint, statusCode: 200, httpVersion: "HTTP/1.1",
headerFields: ["Content-Type": "text/event-stream"])!
return (response, sseEventData)
return (response, sseEventData) // Will return empty Data for SSE
}
try await transport.connect()
try await Task.sleep(for: .milliseconds(100))
let stream = await transport.receive()
@ -442,7 +463,10 @@ import Testing
let receivedData = try await iterator.next()
#expect(receivedData == expectedData)
await transport.disconnect()
}
@Test("Receive Server-Sent Event (SSE) (CR-NL)", .httpClientTransportSetup)
func testReceiveSSE_CRNL() async throws {
let configuration = URLSessionConfiguration.ephemeral
@ -452,24 +476,46 @@ import Testing
endpoint: testEndpoint,
configuration: configuration,
streaming: true,
sseInitializationTimeout: 1,
logger: nil
)
let eventString = "id: event1\r\ndata: {\"key\":\"value\"}\r\n\n"
let sseEventData = eventString.data(using: .utf8)!
// First, set up a handler for the initial POST that will provide a session ID
// Use text/plain to prevent its (empty) body from being yielded to messageStream
await MockURLProtocol.requestHandlerStorage.setHandler {
[testEndpoint] (request: URLRequest) in
let response = HTTPURLResponse(
url: testEndpoint, statusCode: 200, httpVersion: "HTTP/1.1",
headerFields: [
"Content-Type": "text/plain",
"Mcp-Session-Id": "test-session-123",
])!
return (response, Data())
}
// Connect and send a dummy message to get the session ID
try await transport.connect()
try await transport.send(Data())
// Now set up the handler for the SSE GET request
await MockURLProtocol.requestHandlerStorage.setHandler {
[testEndpoint, sseEventData] (request: URLRequest) in
#expect(request.url == testEndpoint)
#expect(request.httpMethod == "GET")
#expect(request.value(forHTTPHeaderField: "Accept") == "text/event-stream")
#expect(
request.value(forHTTPHeaderField: "Mcp-Session-Id") == "test-session-123")
let response = HTTPURLResponse(
url: testEndpoint, statusCode: 200, httpVersion: "HTTP/1.1",
headerFields: ["Content-Type": "text/event-stream"])!
return (response, sseEventData)
}
try await transport.connect()
try await Task.sleep(for: .milliseconds(100))
let stream = await transport.receive()
@ -479,6 +525,8 @@ import Testing
let receivedData = try await iterator.next()
#expect(receivedData == expectedData)
await transport.disconnect()
}
#endif // !canImport(FoundationNetworking)