Skip to content

Feat: Add HTTPResponseError for HTTP failures, including streaming - #491

Open
luis-silva-ms wants to merge 2 commits into
MacPaw:mainfrom
luis-silva-ms:feat/add-http-response-error
Open

luis-silva-ms wants to merge 2 commits into
MacPaw:mainfrom
luis-silva-ms:feat/add-http-response-error

Conversation

@luis-silva-ms

@luis-silva-ms luis-silva-ms commented Nov 11, 2026 •

Copy link
Copy Markdown

TL;DR: Add HTTPResponseError that keeps the status code and body of a (>=400) response so custom errors are not lost.

Why

When a request fails at the HTTP level, callers often can't read the error:

  • Custom error JSON is lost. Gateways and OpenAI-compatible servers often return their own error JSON, for example {"error":{"code":"…","message":"…"}} without type, or a flat {"code":"…","message":"…"}. APIError requires type, so decoding fails. The caller then gets OpenAIError.statusError with only the status code, and the code and message are lost. Non-JSON bodies (HTML, plain text) are lost the same way.
  • Middleware can't work around it for streams. Streaming failures skip response middleware, so a middleware can't read the status code and body of a failed stream.

What

HTTP status failures (status >= 400) now throw a single public error type, HTTPResponseError, for JSON, raw-data and streaming requests.

HTTPResponseError exposes:

  • statusCode and response (HTTPURLResponse)
  • body: the raw error body (Data), kept even when the SDK can't decode it: for example a gateway's custom error JSON, or a non-JSON body
  • apiError: the decoded APIErrorResponse / GeminiAPIErrorResponse when the body is one
  • underlyingError: a transport error that arrived with the failing response

Streaming (StreamingSession) changes:

  • Response middleware now runs for HTTP failures, matching non-streaming requests.
  • SSE/stream middleware is skipped for error bodies, which aren't SSE.
  • Cancellation (CancellationError / URLError.cancelled) is rethrown as is, not wrapped.

⚠️ Behaviour change

There is no source break: swift package diagnose-api-breaking-changes 0.6.0 reports no breaking changes. Only a public type is added.

It is a runtime behaviour change. Code that catches APIErrorResponse, GeminiAPIErrorResponse or OpenAIError.statusError for HTTP failures will silently stop matching. It should catch HTTPResponseError and read apiError / statusCode instead. Errors sent inside a successful (2xx) stream keep their existing types.

Affected Areas

  • Sources/OpenAI/Public/Errors/HTTPResponseError.swift: new public type
  • Sources/OpenAI/Private/URLResponseHandler.swift: non-streaming error path
  • Sources/OpenAI/Private/Streaming/StreamingSession.swift: streaming error path
  • Tests:
  • README, CHANGELOG, Demo (ResponsesStore status hints)

Validation

  • swift build: ✅ passed
  • swift test: ✅ passed (277 XCTest, 0 failures; 35 Swift Testing tests passed)
  • xcodebuild -project Demo/Demo.xcodeproj -scheme Demo -destination "generic/platform=iOS Simulator" build: ✅ BUILD SUCCEEDED
  • swift package diagnose-api-breaking-changes 0.6.0: ✅ No breaking changes detected in OpenAI
  • Linux: not run locally. New and changed files that use HTTPURLResponse / URLRequest import FoundationNetworking under #if canImport(FoundationNetworking).

HTTP status failures (>= 400) from JSON, raw-data and streaming
requests now throw a single HTTPResponseError carrying the status
code, the HTTP response, the raw body (truncated for streams), the
decoded API error when available and any underlying transport error.

Streaming failures now run response middleware, skip SSE middleware
for error bodies, cap the collected error body, and rethrow
cancellation unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant