Skip to content

fix: surface actionable API errors instead of raw JSON dumps (BUG-2046)#30

Merged
ClayMav merged 5 commits into
mainfrom
clay/actionable-api-error-surfacing
Jun 12, 2026
Merged

fix: surface actionable API errors instead of raw JSON dumps (BUG-2046)#30
ClayMav merged 5 commits into
mainfrom
clay/actionable-api-error-surfacing

Conversation

@ClayMav

@ClayMav ClayMav commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

Fixes BUG-2046: job-runs create failed during script upload with a generic request failed with HTTP 400: {raw json blob}, burying the only actionable part (InvalidInputException (No storage source found for bucket: ...)) and giving no hint about which step failed or what upload target was attempted.

The CLI builds the upload key correctly (verified against the live OpenAPI spec); the 400 is a server-side storage-source decision. This PR fixes the CLI-side problem: error surfacing.

Before

request failed with HTTP 400: {"errors":[{"code":"BAD_REQUEST_ERROR","message":"Bad Request","details":"InvalidInputException (No storage source found for bucket: qni7xwfc8m)","path":"/files/upload-url","suggestion":"Update your request and try again."}],"requestId":"3629d..."}

After (curated job-runs commands)

requesting upload URL for s3://wbts-wbc-bznimkgwml/qni7xwfc8m/data/shared/scripts/my-job/script.py: request failed with HTTP 400 (BAD_REQUEST_ERROR)
  message:    Bad Request
  details:    InvalidInputException (No storage source found for bucket: qni7xwfc8m)
  suggestion: Update your request and try again.
  request id: 3629d5f8a10139a4867c043509678f05

The upload path s3://wbts-wbc-bznimkgwml/... is not a registered storage source for your organization. To fix this:
  - omit --upload-path to upload to your organization's managed storage, or
  - use a path under a registered storage integration

After (dynamic api ... commands)

Machine-oriented api commands already emit success responses as raw JSON; they now emit the error envelope as raw, parseable JSON too:

{"errors":[{"code":"BAD_REQUEST_ERROR",...}],"requestId":"..."}

Commits

  • feat(executor): render API error envelopes as readable errors — parse the standard envelope in HTTPError.Error(), multi-line rendering, exact fallback to prior strings for empty/non-JSON/non-envelope bodies; adds APIErrorDetails and JSONError
  • feat(api): emit raw JSON error envelope from dynamic api commands — wrap HTTPError in JSONError in the generated-command path
  • fix(jobs): explain upload-url failures with target and guidanceprepareScript wraps upload-url errors with the attempted s3://bucket/key; storage-source guidance only when an explicit --upload-path/WHEROBOTS_UPLOAD_PATH override hit a "No storage source found" error
  • fix(executor): require standard shape before treating body as envelope — review follow-up: foreign {"errors":["..."]} payloads fall back to the raw body

Compatibility

  • Exit codes unchanged; errors stay on stderr, success output untouched
  • *executor.HTTPError struct unchanged; wrapping happens after execWithRetry, so isRetryable's direct type assertion is unaffected (covered by tests)
  • Empty/non-JSON/non-envelope bodies keep byte-identical prior error strings (covered by tests)

Testing

  • 12 new tests: envelope formatting + all fallbacks, JSONError unwrap semantics, api-command raw-JSON error, and 3 httptest job-runs create scenarios (explicit override w/ guidance, non-envelope w/o guidance, managed default w/o guidance)
  • go test ./..., go vet, gofmt, pre-commit run --all-files all clean
  • Manual: ran the exact ticket command against production — context line + multi-line envelope render correctly end-to-end

Deferred

Up-front client-side validation of --upload-path against org storage sources was considered and deliberately skipped: the server is the source of truth, pre-validation risks false blocks, and it would add API calls to a path that intentionally skips them. A non-blocking warning could be a follow-up if desired.

ClayMav added 4 commits June 3, 2026 17:52
HTTPError.Error() previously dumped the raw JSON response body on one
line, burying the actionable details/suggestion fields of the standard
Wherobots error envelope. Parse the envelope and render it multi-line
(code, message, details, suggestion, request id), falling back to the
exact prior strings for empty, non-JSON, or non-envelope bodies.

Also add APIErrorDetails for callers that need to inspect the details
field, and JSONError for machine-oriented callers that want the raw
envelope body verbatim.
Dynamic api commands are machine-oriented and already write success
responses as raw JSON to stdout. Match that on failure: wrap HTTPError
in executor.JSONError so the API error envelope is printed verbatim
(parseable JSON on stderr) instead of the human-readable rendering.
Unwrap() preserves the *HTTPError for errors.As callers.
BUG-2046: job-runs create failed during script upload with a generic
'request failed with HTTP 400: {raw json}' that hid the actionable
detail (No storage source found for bucket: ...) and gave no hint
about which step failed or what target was attempted.

Wrap upload-url request errors with the attempted s3://bucket/key, and
when the API reports a missing storage source for an explicit
--upload-path / WHEROBOTS_UPLOAD_PATH override, append guidance to
omit the override or use a registered storage integration. Wrapping
uses %w so isRetryable's *HTTPError handling is unaffected.
Review follow-up: a foreign payload like {"errors":["boom"]} previously
matched the envelope detection and rendered with no detail, losing the
raw body. Require at least one errors[] entry with a code or message,
and avoid re-parsing the gjson array twice.
@notion-workspace

Copy link
Copy Markdown

@ClayMav ClayMav marked this pull request as ready for review June 4, 2026 01:06
@ClayMav ClayMav requested a review from a team as a code owner June 4, 2026 01:06

@salty-hambot salty-hambot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed by Salty Hambot 🤖🧂

Solid error-handling plumbing with good test coverage. One real callout: the wrapUploadURLError guidance hinges on a fragile English substring match against server prose — worth a comment acknowledging that tradeoff.

2 finding(s) posted · 2 filtered as false positives.
💬 To request a re-review, comment @salty-hambot review

Comment thread internal/commands/jobs.go
Comment thread internal/executor/request.go
sfishel18
sfishel18 previously approved these changes Jun 4, 2026
Comment thread internal/commands/jobs.go Outdated
Comment thread internal/commands/jobs.go Outdated
…lures

Address review feedback on the actionable-error surfacing for local
script uploads:

- Thread the upload-path override source (flag vs WHEROBOTS_UPLOAD_PATH
  env var) down to wrapUploadURLError so the "missing storage source"
  guidance names the right knob: "omit --upload-path" for the flag,
  "unset WHEROBOTS_UPLOAD_PATH" for the env var. Previously the env-var
  case gave incorrect "omit --upload-path" advice.
- Document the fragile free-text match for the "no storage source found"
  detail with a NOTE explaining the tradeoff (additive guidance, never
  errors on a miss).
- Wrap the presigned-PUT upload failure with the same
  "uploading script to s3://bucket/key" context the upload-URL path got.
- Add TestJobsRunUploadURLStorageSourceErrorEnvOverrideIsActionable
  mirroring the flag test but driving the override via the env var and
  asserting the env-appropriate wording (no misleading "omit
  --upload-path").
@ClayMav ClayMav merged commit 13f4275 into main Jun 12, 2026
3 checks passed
@ClayMav ClayMav deleted the clay/actionable-api-error-surfacing branch June 12, 2026 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants