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:
parent
3ff1085e01
commit
bdaa2585c5
1 changed files with 237 additions and 4 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue