Fix event stream line parsing to handle CRLF and empty lines correctly (#89)
* Optimized code in `connectToEventStream` to use `.lines` computed value of `AsyncSequence` to produce a `AsyncLineSequence` instead of building a lines manually.
* - Reverter use of `lines` (it swallows newlines)
- Fiexed original issue I experienced with line 229 `while let newlineIndex = buffer.firstIndex(of: "\n") {` not handling "\r\n" properly
- Added unit test to test for "\r\n" scenario
- Simplified `end of event` test - `line` creation trims newline, added code trims carriage return. Only thing that should be left is an empty line
This commit is contained in:
parent
99517b0564
commit
f6e401aa97
2 changed files with 45 additions and 3 deletions
|
|
@ -226,12 +226,16 @@ public actor HTTPClientTransport: Actor, Transport {
|
||||||
buffer.append(char)
|
buffer.append(char)
|
||||||
|
|
||||||
// Process complete lines
|
// Process complete lines
|
||||||
while let newlineIndex = buffer.firstIndex(of: "\n") {
|
while let newlineIndex = buffer.utf8.firstIndex(where: { $0 == 10 }) {
|
||||||
let line = buffer[..<newlineIndex]
|
var line = buffer[..<newlineIndex]
|
||||||
|
if line.hasSuffix("\r") {
|
||||||
|
line = line.dropLast()
|
||||||
|
}
|
||||||
|
|
||||||
buffer = String(buffer[buffer.index(after: newlineIndex)...])
|
buffer = String(buffer[buffer.index(after: newlineIndex)...])
|
||||||
|
|
||||||
// Empty line marks the end of an event
|
// Empty line marks the end of an event
|
||||||
if line.isEmpty || line == "\r" || line == "\n" || line == "\r\n" {
|
if line.isEmpty {
|
||||||
if !eventData.isEmpty {
|
if !eventData.isEmpty {
|
||||||
// Process the event
|
// Process the event
|
||||||
if eventType == "id" {
|
if eventType == "id" {
|
||||||
|
|
@ -256,6 +260,10 @@ public actor HTTPClientTransport: Actor, Transport {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if line.hasSuffix("\r") {
|
||||||
|
line = line.dropLast()
|
||||||
|
}
|
||||||
|
|
||||||
// Lines starting with ":" are comments
|
// Lines starting with ":" are comments
|
||||||
if line.hasPrefix(":") { continue }
|
if line.hasPrefix(":") { continue }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,40 @@ import Testing
|
||||||
let expectedData = #"{"key":"value"}"#.data(using: .utf8)!
|
let expectedData = #"{"key":"value"}"#.data(using: .utf8)!
|
||||||
let receivedData = try await iterator.next()
|
let receivedData = try await iterator.next()
|
||||||
|
|
||||||
|
#expect(receivedData == expectedData)
|
||||||
|
}
|
||||||
|
@Test("Receive Server-Sent Event (SSE) (CR-NL)", .httpClientTransportSetup)
|
||||||
|
func testReceiveSSE_CRNL() async throws {
|
||||||
|
let configuration = URLSessionConfiguration.ephemeral
|
||||||
|
configuration.protocolClasses = [MockURLProtocol.self]
|
||||||
|
|
||||||
|
let transport = HTTPClientTransport(
|
||||||
|
endpoint: testEndpoint, configuration: configuration, streaming: true,
|
||||||
|
logger: nil)
|
||||||
|
|
||||||
|
let eventString = "id: event1\r\ndata: {\"key\":\"value\"}\r\n\n"
|
||||||
|
let sseEventData = eventString.data(using: .utf8)!
|
||||||
|
|
||||||
|
await MockURLProtocol.requestHandlerStorage.setHandler {
|
||||||
|
[testEndpoint] (request: URLRequest) in
|
||||||
|
#expect(request.url == testEndpoint)
|
||||||
|
#expect(request.httpMethod == "GET")
|
||||||
|
#expect(request.value(forHTTPHeaderField: "Accept") == "text/event-stream")
|
||||||
|
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()
|
||||||
|
var iterator = stream.makeAsyncIterator()
|
||||||
|
|
||||||
|
let expectedData = #"{"key":"value"}"#.data(using: .utf8)!
|
||||||
|
let receivedData = try await iterator.next()
|
||||||
|
|
||||||
#expect(receivedData == expectedData)
|
#expect(receivedData == expectedData)
|
||||||
}
|
}
|
||||||
#endif // !canImport(FoundationNetworking)
|
#endif // !canImport(FoundationNetworking)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue