Skip to content

fix: Raise a Seam error for a success response that is malformed - #574

Merged
razor-x merged 1 commit into
mainfrom
claude/ruby-sdk-audit-xbhaia-invalid-response
Nov 5, 2026
Merged

razor-x merged 1 commit into
mainfrom
claude/ruby-sdk-audit-xbhaia-invalid-response

Conversation

@razor-x

@razor-x razor-x commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

Problem

SDK audit finding M3. A 2xx with an unexpected envelope (proxy rewrite, gateway maintenance page with a JSON content type, response-key rename) escaped the SDK's error hierarchy: generated routes did res.body["device"] and the resource loader raised a bare NoMethodError on nil or String, or returned nil silently. A 3xx passed straight through, since Faraday's RaiseError only covers 400–599, so a redirecting endpoint yielded nil instead of an error. The action-attempt poll crashed the same way mid-wait, and the paginator silently ended iteration when the pagination object was missing.

Fix

Ported from seamapi/python#638 and seamapi/php#475, with the same message shape as the other three SDKs:

Seam returned an invalid response for /devices/get: expected "device", which the response does not contain
Seam returned an invalid response for /devices/get: expected "device", got String instead of a response object
Seam returned an invalid response for /devices/list: expected "devices", got Hash instead of a list
Seam returned an invalid response for /devices/get: expected "device", got a 302 response instead of a success response
Seam returned an invalid response for /devices/list: expected "pagination", got String instead of a pagination object
  • New Seam::Http::InvalidResponseError < Seam::Error with path and response_key.
  • BaseResource.from_http_response(res, key, path) and .list_from_http_response(res, key, path) read the Faraday response, check the status, body shape, key, and object-versus-list, and hand the value to the existing load_from_response, which keeps its current meaning for nested and optional values.
  • Template change: every generated route becomes a single Seam::Resources::Device.from_http_response(res, "device", "/devices/get") call, choosing the list form from the blueprint's resource_list response type. Regenerated against current main with zero drift.
  • ActionAttemptResolver polls through ActionAttempt.from_http_response. The paginator validates the pagination envelope itself, so a list route that returns no pagination raises instead of silently stopping.

A 2xx whose body is declared JSON but does not parse still raises Faraday::ParsingError from Faraday's JSON middleware, as before; that case is pinned in the spec.

Tests

New spec/seam_client/invalid_response_spec.rb (WebMock): missing key, wrong key, null body, string body, plain text, non-object under key, non-list under list key, 302, malformed poll response mid-wait, missing and non-object pagination through create_paginator. Every message is pinned exactly. seam_error_spec.rb lists the new class.

Revert-check against main: 11 failures, with the audit's symptoms (undefined method 'each' for nil, undefined method '[]' for nil, and "nothing was raised" for the redirect and pagination cases).

🤖 Generated with Claude Code

https://claude.ai/code/session_01SQW83gyXeUG61RDrHEky97

@razor-x
razor-x force-pushed the claude/ruby-sdk-audit-xbhaia-invalid-response branch 2 times, most recently from 5d5a6a4 to 50aeca0 Compare November 5, 2026 20:13
Every generated route read its resource straight out of res.body, so a
2xx response with an unexpected envelope, a proxy rewrite, a gateway page
with a JSON content type, or a renamed response key surfaced as a bare
NoMethodError on nil or String from deep inside the resource loader, or
as a silent nil. A 3xx passed through untouched because Faraday's
RaiseError only covers 400 through 599. The action attempt poll and the
paginator's pagination envelope failed the same way, the latter by
silently ending iteration.

Add Seam::Http::InvalidResponseError and a Seam::Http::Response helper
that reads the response key for an object, a list, or the pagination
envelope, naming the endpoint, the expected key, and what arrived
instead. Generate every route through it, and route the action attempt
poll and the paginator through the same helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQW83gyXeUG61RDrHEky97
@razor-x
razor-x force-pushed the claude/ruby-sdk-audit-xbhaia-invalid-response branch from 50aeca0 to 45bc9e7 Compare November 5, 2026 20:44
@razor-x
razor-x merged commit e4e4309 into main Nov 5, 2026
17 checks passed
@razor-x
razor-x deleted the claude/ruby-sdk-audit-xbhaia-invalid-response branch November 5, 2026 20:53
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.

2 participants