Skip to content

feat: read what the server said went wrong - #18

Draft
simonx1 wants to merge 1 commit into
obie:mainfrom
simonx1:feat/parse-error-bodies
Draft

simonx1 wants to merge 1 commit into
obie:mainfrom
simonx1:feat/parse-error-bodies

Conversation

@simonx1

@simonx1 simonx1 commented Sep 18, 2026

Copy link
Copy Markdown

Both providers explain their errors. Typesafe's 422 details the offending field; OpenRouter answers with {"error": {"code", "message"}}. Neither reached the exception.

ApiError#message was a constant — "unprocessable entity" — and the explanation sat on #body as a JSON string, so every caller who wanted the reason wrote the same parse, and every log line that printed the exception and not the body threw the reason away.

ApiError now parses a JSON object body into #detail, #error_code and #parsed_body, reading them whether the payload nests them under "error" or puts them at the top level, and appends the detail to the message:

unprocessable entity: questions.sev.criteria must have at least 2 entries

Everything that isn't a JSON object — an HTML error page from a proxy, plain text from a load balancer, an empty body, a JSON array — leaves all three nil and the message exactly as it was. Parsing happens once, lazily, and #body is still the raw string.

Tests

test/error_body_test.rb, 13 cases, most of them about bodies that aren't well-formed error objects.


Draft: part of a security and API-coverage audit, opened for reference rather than as a request for immediate review. Independent of the other branches, each off main. Suite green on Ruby 3.2.11, 3.3.8 and 3.4.8.

🤖 Generated with Claude Code

Both providers explain their errors. Typesafe's 422 details the offending
field; OpenRouter answers with {"error": {"code", "message"}}. Neither
reached the exception. ApiError#message was a constant -- "unprocessable
entity" -- and the explanation sat on #body as a JSON string, so every
caller who wanted the reason wrote the same parse, and every log line
that printed the exception and not the body threw the reason away.

ApiError now parses a JSON object body into #detail, #error_code, and
#parsed_body, reading them whether the payload nests them under "error"
or puts them at the top level, and appends the detail to the message. A
422 reads "unprocessable entity: questions.sev.criteria must have at
least 2 entries".

Everything that is not a JSON object -- an HTML error page from a proxy,
plain text from a load balancer, an empty body, a JSON array -- leaves
all three nil and the message exactly as it was. Parsing happens once,
lazily, and #body is still the raw string it always was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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