diff --git a/CLAUDE.md b/CLAUDE.md index b239a5c3..42214cf6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,3 +19,34 @@ that still calls a removed/renamed API. The goal is for Demo to stay complete and up to date with the package, not silently lag behind. Only skip this build when the change clearly cannot affect Demo (docs, CI config, tests-only changes, etc.). + +## Foundation types that don't exist on Linux + +CI builds and tests this package on Linux (Swift 5.10, 6.0, 6.3) in addition +to Apple platforms. Several `Foundation` types you'd normally reach for +without a second thought — `HTTPURLResponse`, `URLSession.*` nested types +(e.g. `ResponseDisposition`), `URLRequest` in some contexts, etc. — live in +the separate `FoundationNetworking` module on Linux, not in `Foundation` +itself. Code that uses them compiles fine on macOS and fails on Linux CI +only, with an error like `'HTTPURLResponse' is unavailable: This type has +moved to the FoundationNetworking module.` + +Whenever a new file (source *or* test) references one of these types, guard +the import the way existing files already do: + +```swift +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +``` + +`swift build` / `swift test` passing locally on macOS does not confirm this — +the failure only shows up on Linux. If you can't run the Linux job yourself, +at least double-check new files against this pattern before considering the +change done. + +## Wait for explicit instructions to push + +CI takes several minutes to run, so commit locally as you go but don't run +`git push` unless the user explicitly asks for it at that point. diff --git a/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7b68eb0e..0246b097 100644 --- a/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "bcd79666ae410601a4f1ded1094921cb30aa100078efb734690a128ae00006a7", + "originHash" : "413e0f11af60153179b28c3af1a3e2f551ab5eec31cb11e96335bcd5b4cb99e9", "pins" : [ { "identity" : "activityindicatorview", "kind" : "remoteSourceControl", - "location" : "https://github.com/exyte/ActivityIndicatorView", + "location" : "https://github.com/exyte/ActivityIndicatorView.git", "state" : { - "revision" : "36140867802ae4a1d2b11490bcbbefe058001d14", - "version" : "1.2.1" + "revision" : "41f89372c018decaf965e8dddeb6e5347741beb4", + "version" : "2.0.2" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/exyte/AnchoredPopup.git", "state" : { - "revision" : "2fb9d1ac101b86cbcc12a3f8e571648ce4469d18", - "version" : "1.1.3" + "revision" : "8051eae56e20567b52b1ee30261d4b292a081da0", + "version" : "1.2.2" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/exyte/Chat.git", "state" : { - "revision" : "056ee35312811844a5d257bad076423f69f4b707", - "version" : "2.7.10" + "revision" : "60ff30bc7065581d3e231fd475b269d416b427f5", + "version" : "3.3.3" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mattt/eventsource.git", "state" : { - "revision" : "a3a85a85214caf642abaa96ae664e4c772a59f6e", - "version" : "1.4.1" + "revision" : "86b5096ac59ab46e66bd1f6377c604bc1dab0bc2", + "version" : "1.5.1" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Giphy/giphy-ios-sdk", "state" : { - "revision" : "3ba672878445c53c8cb87d57c3ae5cc1f3166b5b", - "version" : "2.3.2" + "revision" : "37f5b1ff6cf8bc4a78c0bc5eb1b814381bac9580", + "version" : "2.2.16" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Kingfisher", "state" : { - "revision" : "cf8be20d07654570554c8a8a4952bc8a5766a8b0", - "version" : "8.9.0" + "revision" : "be0d257b9bd47a4e6e1265fc8c237411825f107a", + "version" : "8.12.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/libwebp-Xcode", "state" : { - "revision" : "0d60654eeefd5d7d2bef3835804892c40225e8b2", - "version" : "1.5.0" + "revision" : "2b5256c29ff4e20f2a0d5ee863b62b1a22144434", + "version" : "1.6.0" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/exyte/MediaPicker.git", "state" : { - "revision" : "ce2eda6300337d1478a78fc033bce8dd9bf4bb2c", - "version" : "3.3.2" + "revision" : "96dc0e21adaa9266bbb5698052c7ddfd22f69f0c", + "version" : "3.4.6" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", - "version" : "1.3.0" + "revision" : "0442cb5a3f98ab802acb777929fdb446bda11a34", + "version" : "1.3.1" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "fea17c02d767f46b23070fdfdacc28a03a39232a", - "version" : "1.5.1" + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types", "state" : { - "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", - "version" : "1.5.1" + "revision" : "bff4b6903cdc99dda49649dd52f46c11cfd3ed50", + "version" : "1.8.0" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "5073617dac96330a486245e4c0179cb0a6fd2256", - "version" : "1.12.0" + "revision" : "9c6fb14227f55d8f711ce3847dc2f419fb0ecacb", + "version" : "1.15.1" } }, { @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "f71c8d2a5e74a2c6d11a0fbe324774b5d6084237", - "version" : "2.99.0" + "revision" : "21de5f08c1a166a6dd293d0e587ad977bf8dac5d", + "version" : "2.103.0" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-openapi-runtime", "state" : { - "revision" : "f039fa6d6338aab5164f3d1be16281524c9a8f89", - "version" : "1.11.0" + "revision" : "9e53df53e043b53046a371b31aaf0a1e26eef946", + "version" : "1.12.1" } }, { @@ -141,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df", - "version" : "1.6.4" + "revision" : "869129b7bf4ecc57b97d0193ad29690ca2134750", + "version" : "1.8.1" } } ], diff --git a/Demo/DemoChat/Package.resolved b/Demo/DemoChat/Package.resolved index 7b68eb0e..0246b097 100644 --- a/Demo/DemoChat/Package.resolved +++ b/Demo/DemoChat/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "bcd79666ae410601a4f1ded1094921cb30aa100078efb734690a128ae00006a7", + "originHash" : "413e0f11af60153179b28c3af1a3e2f551ab5eec31cb11e96335bcd5b4cb99e9", "pins" : [ { "identity" : "activityindicatorview", "kind" : "remoteSourceControl", - "location" : "https://github.com/exyte/ActivityIndicatorView", + "location" : "https://github.com/exyte/ActivityIndicatorView.git", "state" : { - "revision" : "36140867802ae4a1d2b11490bcbbefe058001d14", - "version" : "1.2.1" + "revision" : "41f89372c018decaf965e8dddeb6e5347741beb4", + "version" : "2.0.2" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/exyte/AnchoredPopup.git", "state" : { - "revision" : "2fb9d1ac101b86cbcc12a3f8e571648ce4469d18", - "version" : "1.1.3" + "revision" : "8051eae56e20567b52b1ee30261d4b292a081da0", + "version" : "1.2.2" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/exyte/Chat.git", "state" : { - "revision" : "056ee35312811844a5d257bad076423f69f4b707", - "version" : "2.7.10" + "revision" : "60ff30bc7065581d3e231fd475b269d416b427f5", + "version" : "3.3.3" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mattt/eventsource.git", "state" : { - "revision" : "a3a85a85214caf642abaa96ae664e4c772a59f6e", - "version" : "1.4.1" + "revision" : "86b5096ac59ab46e66bd1f6377c604bc1dab0bc2", + "version" : "1.5.1" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Giphy/giphy-ios-sdk", "state" : { - "revision" : "3ba672878445c53c8cb87d57c3ae5cc1f3166b5b", - "version" : "2.3.2" + "revision" : "37f5b1ff6cf8bc4a78c0bc5eb1b814381bac9580", + "version" : "2.2.16" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Kingfisher", "state" : { - "revision" : "cf8be20d07654570554c8a8a4952bc8a5766a8b0", - "version" : "8.9.0" + "revision" : "be0d257b9bd47a4e6e1265fc8c237411825f107a", + "version" : "8.12.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/libwebp-Xcode", "state" : { - "revision" : "0d60654eeefd5d7d2bef3835804892c40225e8b2", - "version" : "1.5.0" + "revision" : "2b5256c29ff4e20f2a0d5ee863b62b1a22144434", + "version" : "1.6.0" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/exyte/MediaPicker.git", "state" : { - "revision" : "ce2eda6300337d1478a78fc033bce8dd9bf4bb2c", - "version" : "3.3.2" + "revision" : "96dc0e21adaa9266bbb5698052c7ddfd22f69f0c", + "version" : "3.4.6" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", - "version" : "1.3.0" + "revision" : "0442cb5a3f98ab802acb777929fdb446bda11a34", + "version" : "1.3.1" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "fea17c02d767f46b23070fdfdacc28a03a39232a", - "version" : "1.5.1" + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types", "state" : { - "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", - "version" : "1.5.1" + "revision" : "bff4b6903cdc99dda49649dd52f46c11cfd3ed50", + "version" : "1.8.0" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "5073617dac96330a486245e4c0179cb0a6fd2256", - "version" : "1.12.0" + "revision" : "9c6fb14227f55d8f711ce3847dc2f419fb0ecacb", + "version" : "1.15.1" } }, { @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "f71c8d2a5e74a2c6d11a0fbe324774b5d6084237", - "version" : "2.99.0" + "revision" : "21de5f08c1a166a6dd293d0e587ad977bf8dac5d", + "version" : "2.103.0" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-openapi-runtime", "state" : { - "revision" : "f039fa6d6338aab5164f3d1be16281524c9a8f89", - "version" : "1.11.0" + "revision" : "9e53df53e043b53046a371b31aaf0a1e26eef946", + "version" : "1.12.1" } }, { @@ -141,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df", - "version" : "1.6.4" + "revision" : "869129b7bf4ecc57b97d0193ad29690ca2134750", + "version" : "1.8.1" } } ], diff --git a/Demo/DemoChat/Package.swift b/Demo/DemoChat/Package.swift index ec56f58d..f30330f7 100644 --- a/Demo/DemoChat/Package.swift +++ b/Demo/DemoChat/Package.swift @@ -14,8 +14,13 @@ let package = Package( ], dependencies: [ .package(name: "OpenAI", path: "../.."), - .package(url: "https://github.com/exyte/Chat.git", from: "2.5.7"), - .package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.9.0") + .package(url: "https://github.com/exyte/Chat.git", from: "3.3.3"), + .package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.9.0"), + // Pinned explicitly: MediaPicker 3.4.x calls PopupParameters.displayMode(_:), which + // AnchoredPopup only added in 1.2.0. MediaPicker's own manifest still declares + // `from: "1.1.3"`, so without this override SwiftPM's version resolution can pick + // 1.1.3 and fail to build. + .package(url: "https://github.com/exyte/AnchoredPopup.git", from: "1.2.2") ], targets: [ .target( diff --git a/Demo/DemoChat/Sources/ResponsesStore.swift b/Demo/DemoChat/Sources/ResponsesStore.swift index d4e71571..13e1fac4 100644 --- a/Demo/DemoChat/Sources/ResponsesStore.swift +++ b/Demo/DemoChat/Sources/ResponsesStore.swift @@ -11,7 +11,7 @@ import OpenAI @MainActor public final class ResponsesStore: ObservableObject { - struct ConversationTurn: Identifiable, Hashable, Sendable { + struct ConversationTurn: Identifiable, Sendable { public enum TurnType: Sendable { case userInput case response diff --git a/Demo/DemoChat/Sources/UI/DetailView.swift b/Demo/DemoChat/Sources/UI/DetailView.swift index 6c29e855..efa083cb 100644 --- a/Demo/DemoChat/Sources/UI/DetailView.swift +++ b/Demo/DemoChat/Sources/UI/DetailView.swift @@ -154,7 +154,6 @@ struct DetailView: View { } }) .setAvailableInputs([.text, .media]) - .messageUseMarkdown(true) .betweenListAndInputViewBuilder(infoMessage) } diff --git a/Demo/DemoChat/Sources/UI/ResponsesChatDetailView.swift b/Demo/DemoChat/Sources/UI/ResponsesChatDetailView.swift index 3f7736f6..b48311f1 100644 --- a/Demo/DemoChat/Sources/UI/ResponsesChatDetailView.swift +++ b/Demo/DemoChat/Sources/UI/ResponsesChatDetailView.swift @@ -204,7 +204,6 @@ public struct ResponsesChatDetailView: View { } }) .setAvailableInputs([.text, .media]) - .messageUseMarkdown(true) } } diff --git a/Sources/OpenAI/Private/JSONResponseErrorDecoder.swift b/Sources/OpenAI/Private/JSONResponseErrorDecoder.swift index 4d2b1a73..a72d5c77 100644 --- a/Sources/OpenAI/Private/JSONResponseErrorDecoder.swift +++ b/Sources/OpenAI/Private/JSONResponseErrorDecoder.swift @@ -13,8 +13,8 @@ struct JSONResponseErrorDecoder { func decodeErrorResponse(data: Data) -> (any ErrorResponse)? { if let decoded = try? decoder.decode(APIErrorResponse.self, from: data) { return decoded - } else if let decoded = try? decoder.decode([GeminiAPIErrorResponse].self, from: data) { - return decoded[0] + } else if let decoded = try? decoder.decode([GeminiAPIErrorResponse].self, from: data), let first = decoded.first { + return first } else { return nil } diff --git a/Sources/OpenAI/Private/Streaming/StreamingSession.swift b/Sources/OpenAI/Private/Streaming/StreamingSession.swift index b718db6b..4ec4bdb3 100644 --- a/Sources/OpenAI/Private/Streaming/StreamingSession.swift +++ b/Sources/OpenAI/Private/Streaming/StreamingSession.swift @@ -11,6 +11,10 @@ import Foundation import FoundationNetworking #endif +/// Real API error bodies are at most a few KB. Cap how much of a non-2xx body we'll buffer so a +/// malformed or malicious server can't force unbounded memory growth by never ending the response. +private let maxErrorBodyByteCount = 256 * 1024 + final class StreamingSession: NSObject, Identifiable, URLSessionDataDelegateProtocol, @unchecked Sendable { typealias ResultType = Interpreter.ResultType @@ -24,6 +28,11 @@ final class StreamingSession: NSObject, Identifi private let onProcessingError: (@Sendable (StreamingSession, Error) -> Void)? private let onComplete: (@Sendable (StreamingSession, Error?) -> Void)? + /// Set once a response with a non-2xx status code is received. + /// While set, incoming data is treated as an error body rather than being fed to the interpreter. + private var errorResponse: HTTPURLResponse? + private var errorData = Data() + init( urlSessionFactory: URLSessionFactory = FoundationURLSessionFactory(), urlRequest: URLRequest, @@ -55,16 +64,34 @@ final class StreamingSession: NSObject, Identifi func urlSession(_ session: any URLSessionProtocol, task: any URLSessionTaskProtocol, didCompleteWithError error: (any Error)?) { executionSerializer.dispatch { + if let httpResponse = self.errorResponse { + let decodedError = JSONResponseErrorDecoder(decoder: JSONDecoder()).decodeErrorResponse(data: self.errorData) as (any Error)? + let resolvedError = decodedError + ?? OpenAIError.statusError(response: httpResponse, statusCode: httpResponse.statusCode) + self.onProcessingError?(self, resolvedError) + self.onComplete?(self, resolvedError) + return + } self.onComplete?(self,error) } } - + func urlSession(_ session: any URLSessionProtocol, dataTask: any URLSessionDataTaskProtocol, didReceive data: Data) { executionSerializer.dispatch { let data = self.middlewares.reduce(data) { current, middleware in middleware.interceptStreamingData(request: dataTask.originalRequest, current) } - + + if self.errorResponse != nil { + self.errorData.append(data) + if self.errorData.count > maxErrorBodyByteCount { + // Give up on this body: stop letting the server grow it further and let + // didCompleteWithError fall back to statusError with whatever we have. + dataTask.cancel() + } + return + } + self.interpreter.processData(data) } } @@ -77,9 +104,10 @@ final class StreamingSession: NSObject, Identifi ) { executionSerializer.dispatch { if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode >= 400 { - let error = OpenAIError.statusError(response: httpResponse, statusCode: httpResponse.statusCode) - self.onProcessingError?(self, error) - completionHandler(.cancel) + // Keep the connection open so the error body (with the actual failure reason) can be + // read in didReceive(data:) and decoded once the task completes in didCompleteWithError. + self.errorResponse = httpResponse + completionHandler(.allow) return } completionHandler(.allow) diff --git a/Tests/OpenAITests/JSONResponseErrorDecoderTests.swift b/Tests/OpenAITests/JSONResponseErrorDecoderTests.swift new file mode 100644 index 00000000..6df56462 --- /dev/null +++ b/Tests/OpenAITests/JSONResponseErrorDecoderTests.swift @@ -0,0 +1,19 @@ +// +// JSONResponseErrorDecoderTests.swift +// OpenAI +// + +import XCTest +@testable import OpenAI + +final class JSONResponseErrorDecoderTests: XCTestCase { + private let decoder = JSONResponseErrorDecoder(decoder: JSONDecoder()) + + // A body that decodes as an empty Gemini error array must not be treated as a + // single Gemini error: decodeErrorResponse indexes element 0 unconditionally, + // which traps on `[]` instead of falling through to the statusError fallback. + func testDecodeErrorResponseDoesNotCrashOnEmptyArrayBody() { + let emptyArrayBody = Data("[]".utf8) + XCTAssertNil(decoder.decodeErrorResponse(data: emptyArrayBody)) + } +} diff --git a/Tests/OpenAITests/StreamingSessionErrorHandlingTests.swift b/Tests/OpenAITests/StreamingSessionErrorHandlingTests.swift new file mode 100644 index 00000000..e49ff9ae --- /dev/null +++ b/Tests/OpenAITests/StreamingSessionErrorHandlingTests.swift @@ -0,0 +1,157 @@ +// +// StreamingSessionErrorHandlingTests.swift +// OpenAI +// + +import XCTest +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +@testable import OpenAI + +final class StreamingSessionErrorHandlingTests: XCTestCase { + private let urlSessionFactory = MockURLSessionFactory() + private let streamInterpreter = MockDataStreamInterpreter() + private let executionSerializer: ExecutionSerializer = NoDispatchExecutionSerializer() + + private var receivedContentCount = 0 + private var processingErrors: [Error] = [] + private var completionErrors: [Error?] = [] + private var receivedDispositions: [URLSession.ResponseDisposition] = [] + + private lazy var streamingSession = StreamingSession( + urlSessionFactory: urlSessionFactory, + urlRequest: .init(url: URL(string: "https://api.openai.com/v1/chat/completions")!), + interpreter: streamInterpreter, + sslDelegate: nil, + middlewares: [], + executionSerializer: executionSerializer, + onReceiveContent: { _, _ in + self.receivedContentCount += 1 + }, + onProcessingError: { _, error in + self.processingErrors.append(error) + }, + onComplete: { _, error in + self.completionErrors.append(error) + } + ) + + private func makeErrorResponse(statusCode: Int = 400) -> HTTPURLResponse { + HTTPURLResponse( + url: URL(string: "https://api.openai.com/v1/chat/completions")!, + statusCode: statusCode, + httpVersion: nil, + headerFields: nil + )! + } + + func testErrorBodyIsDecodedInsteadOfBeingDiscarded() throws { + _ = streamingSession + let dataTask = DataTaskMock() + + streamingSession.urlSession( + urlSessionFactory.urlSession, + dataTask: dataTask, + didReceive: makeErrorResponse() + ) { self.receivedDispositions.append($0) } + + // The connection must be kept open (not cancelled) so the error body can still be read. + XCTAssertEqual(receivedDispositions, [.allow]) + + let errorBody = """ + {"error": {"message": "The model `gpt-5.6-terra` does not exist", "type": "invalid_request_error", "param": null, "code": "model_not_found"}} + """.data(using: .utf8)! + + // Body can arrive in multiple chunks; make sure they're accumulated correctly. + let midpoint = errorBody.index(errorBody.startIndex, offsetBy: errorBody.count / 2) + streamingSession.urlSession(urlSessionFactory.urlSession, dataTask: dataTask, didReceive: Data(errorBody[..