Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/heavy-pianos-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@germ-network/atprototypes": patch
---

preserve the body-decode error when a recognized response fails to parse
9 changes: 8 additions & 1 deletion Sources/AtprotoTypes/XRPC/ResponseParsing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,20 @@ extension Atproto.XRPC {
error: ErrorResponse
)
case unrecognized(HTTPResponse)
//A recognized status whose body failed to decode. Distinct from .unrecognized
//so a well-formed 200 with an undecodable body (e.g. a record whose format
//drifted) keeps its DecodingError — coding path and all — instead of being
//reported identically to an unknown status.
case undecodable(HTTPResponse, underlying: any Error)

public var errorDescription: String? {
switch self {
case .xrpcError(let status, let error):
"\(status): \(error.error)"
case .unrecognized(let response):
"Unrecognized response: \(response)"
case .undecodable(let response, let underlying):
"Undecodable response: \(response), underlying: \(underlying)"
}
}
}
Expand Down Expand Up @@ -105,7 +112,7 @@ extension Atproto.XRPC.ResponseParsing {

return .error(.unrecognized(fullResponse.response))
} catch {
return .error(.unrecognized(fullResponse.response))
return .error(.undecodable(fullResponse.response, underlying: error))
}
}

Expand Down
58 changes: 58 additions & 0 deletions Tests/AtprotoTypesTests/ResponseParsingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// ResponseParsingTests.swift
// AtprotoTypes
//
// Created by Mark @ Germ on 7/15/26.
//

import AtprotoTypes
import Foundation
import GermConvenience
import HTTPTypes
import Testing

struct ResponseParsingTests {
private struct Body: Decodable, Sendable {
let name: String
}

private enum MockParsing: Atproto.XRPC.ResponseParsing {
typealias Output = Body
static var badRequestErrors: Set<String> { defaultErrors }
}

//A 200 whose body fails to decode must keep the DecodingError (coding path
//and all) rather than collapsing into .unrecognized, which reports nothing
//beyond the response headers.
@Test func undecodableBodyPreservesUnderlyingError() throws {
let response = HTTPDataResponse(
data: Data("{\"name\": 42}".utf8),
response: .init(status: .ok)
)

let parsed = try MockParsing.parse(fullResponse: response)
guard case .error(.undecodable(_, let underlying)) = parsed else {
Issue.record("expected .undecodable, got \(parsed)")
return
}
guard case DecodingError.typeMismatch(_, let context) = underlying else {
Issue.record("expected DecodingError.typeMismatch, got \(underlying)")
return
}
#expect(context.codingPath.map(\.stringValue) == ["name"])
}

//an unknown status without a decode attempt still reports .unrecognized
@Test func unknownStatusReportsUnrecognized() throws {
let response = HTTPDataResponse(
data: Data(),
response: .init(status: .init(code: 418))
)

let parsed = try MockParsing.parse(fullResponse: response)
guard case .error(.unrecognized) = parsed else {
Issue.record("expected .unrecognized, got \(parsed)")
return
}
}
}
Loading