fix: surface the API's error message instead of the client's status name - #53
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 55 |
| Duplication | 5 |
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull Request Overview
The API error handling can throw while processing malformed errors entries, preventing the original API failure from being reported. This should be fixed before merging. Codacy reports the PR as not up to standards due to a new issue; the same function also has high decision complexity, increasing regression risk.
About this PR
- Add an import-config regression test covering tools import failure extraction through the shared helper. Per-line coverage is unavailable, so coverage claims cannot currently be independently verified.
Test suggestions
- Extract the API message field, errors array entries, plain string body, and serialized fallback
- Return no details for empty, null, absent, or empty-string bodies
- Format an ApiError with the server message and trailing HTTP status
- Preserve legacy output when the body is empty or only repeats the status name
- Sanitize terminal escape sequences in server-provided error text
- Format ordinary Error and non-Error throws
- Handle an API error by printing the formatted message and exiting with status 1
- Retain tools import failure extraction behavior through the shared apiErrorDetails helper
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Retain tools import failure extraction behavior through the shared apiErrorDetails helper
Low confidence findings
- Reconcile the stated count of 13 new tests with the 10 test cases present in the provided test file.
TIP How was this review? Give us feedback
| export function apiErrorDetails(body: unknown): string[] { | ||
| if (body && typeof body === "object") { | ||
| const details: string[] = []; | ||
| const obj = body as Record<string, unknown>; | ||
| if (typeof obj.message === "string") { | ||
| details.push(obj.message); | ||
| } | ||
| if (Array.isArray(obj.errors)) { | ||
| for (const e of obj.errors) { | ||
| details.push(typeof e === "string" ? e : ((e as any)?.message ?? JSON.stringify(e))); | ||
| } | ||
| } | ||
| if (details.length === 0) { | ||
| const serialized = JSON.stringify(body); | ||
| if (serialized !== "{}" && serialized !== "null") { | ||
| details.push(serialized); | ||
| } | ||
| } | ||
| return details; | ||
| } | ||
| if (typeof body === "string" && body.length > 0) { | ||
| return [body]; | ||
| } | ||
| return []; | ||
| } |
There was a problem hiding this comment.
🟡 MEDIUM RISK
Malformed errors entries can leave non-string values such as undefined in details; formatError then calls .trim() and throws while handling the original API error. Normalize every entry to a string or filter invalid entries before formatting. Given this function's 15 decision paths, extract the body/array normalization into helpers and add tests for malformed entries.
There was a problem hiding this comment.
Fixed. Every errors entry now leaves errorEntryDetail as a string, so a body like {"errors": [{"message": 42}]} can no longer throw on .trim() while reporting the API error the user was waiting to read; entries with nothing to say are dropped rather than rendered as "undefined". The function was also split — objectDetails, errorEntryDetail and serializedBody — and there is a regression test for malformed entries.
🤖 Generated by /pr-fixup command
The merge-base changed after approval.
1a70ba6 to
1b470c5
Compare
bc88271 to
988a191
Compare
`catchErrorCodes` builds ApiError.message from a static
{400: 'Bad Request', 404: 'Not Found', ...} table, so handleError - which
printed err.message and ignored err.body - was throwing away the explanation
the server had already sent. On every command.
Error: Not Found
Error: Could not find repository gh/claudiacodacy/no-such-repo (HTTP 404)
Error: Unauthorized
Error: Bad credentials (HTTP 401)
Error: Bad Request
Error: SBOM tag mismatch: expected 9.9.9, found 3.20 (HTTP 400)
The status stays as a trailing (HTTP nnn): it is what gets quoted in a bug
report, and "Unauthorized" carries meaning a terse body may not.
The extraction moves rather than duplicates. import-config.ts already had
parseApiErrorBody for the `tools --import` failure table; it is now
apiErrorDetails in utils/error.ts, used by both, so a new body shape has one
place to be handled.
The message is sanitized before printing. Error bodies quote back image
names, tags and branch names, which a crafted repository controls, and this
was the one render path with no sanitizer - because it never rendered server
text before.
A body that carries nothing, or only echoes the status name, leaves the old
output untouched, so commands whose failures the API does not explain are
byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`apiErrorDetails` pushed an `errors` entry's `message` through unconverted.
A body like `{"errors": [{"message": 42}]}` therefore left a number in the
list, and `formatError`'s `.trim()` threw on it — an exception raised while
reporting an API error, which loses the error the user was waiting to read.
Every entry now leaves `errorEntryDetail` as a string, and the ones with
nothing to say are dropped rather than rendered as "undefined".
Codacy also had the function at CCN 15 (limit 8). The body/array normalization
and the serialized fallback come out as `objectDetails`, `errorEntryDetail`
and `serializedBody`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
988a191 to
806a763
Compare
Code reviewReviewed as part of the #49 → #50 → #52 → #53 stack, against the merged state. The core change is sound — A non-JSON error body is passed through verbatim, with no length cap codacy-cloud-cli/src/utils/error.ts Lines 28 to 32 in 806a763
// src/api/client/core/request.ts (generated, gitignored) — getResponseBody
const isJSON = jsonTypes.some(type => contentType.toLowerCase().startsWith(type));
if (isJSON) { return await response.json(); } else { return await response.text(); }So the whole response body becomes the single "detail" and Why it matters: this is a regression in exactly the environments the proxy support was added for, and it is the one input class the new tests do not cover — Expected fix: only treat a string body as a detail when it is plausibly a message — e.g. cap it (first line, or ~200 characters) and fall back to 🤖 Generated by /code-review command |
…sages # Conflicts: # SPECS/README.md
… failures ApiError.body is response.text() for any content type the generated client does not treat as JSON, so a gateway, load balancer or TLS-intercepting proxy answering 502 with an HTML error page put the whole page in the terminal behind `Error: ` — a regression in exactly the environments the HTTPS_PROXY support was added for, where the previous behaviour was `Error: Bad Gateway`. A string body is now used only when it plausibly is a message: first line, at most 200 characters, not starting with `<`. Anything else falls back to the status name. Dropping rather than truncating, because half an HTML page is noise, not a shorter explanation. The same cap guards the serializedBody fallback, which can produce an equally large single line. Separately, --keep-latest's per-tag failure list still read "Bad Request" for every failure. That loop continues past a failure so it can list every tag that did not go, so it formats at its own call site and never reaches formatError. New exported errorReason(err) shares the extraction with formatError — both now go through a private apiErrorReason — and returns the bare reason, without the `Error: ` prefix, the `(HTTP n)` suffix or sanitization, since the table path sanitizes at render and the JSON path does not sanitize at all. The existing test masked this by rejecting with a plain Error rather than an ApiError. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 705 and 738 totals were measured per branch; in merged order the three follow-up entries run 747 -> 752 -> 762, and 762 is what the tip runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Confirmed and fixed in abecad4, plus #52's third finding, which lands here rather than there. Non-JSON error body passed through verbatim — fixed. Confirmed against A string body is now used only when it plausibly is a message: first line, at most 200 characters ( I went with dropping rather than truncating, which is the one place I'd want a second opinion: half an HTML page or half a stack trace is noise rather than a shorter explanation, and falling back gives exactly the pre-PR behaviour, which was at least correct. The cost is that a >200-character plain-text message is discarded rather than clipped — I think that's the right trade, since the API's own messages are one short line, but say if you'd rather see a clipped prefix. Same cap now guards the #52's finding 3 — per-tag failures reported the status name — fixed here.
Every fix across the stack was checked by reverting it and confirming the new test fails — all six caught their own bug. 762 tests green on this branch, 🤖 Generated by /pr-fixup command |
Pull Request is not mergeable
…sages Resolves the changelog conflict in stack order and trims this branch's review-follow-up entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pull Request is not mergeable
Stacked on #52 (→ #50 → #49). Found while testing the image commands end to end against
gh/claudiacodacy.The bug
catchErrorCodesin the generated client buildsApiError.messagefrom a static table:handleErrorprinted that and ignorederr.body— where the server's actual explanation was sitting the whole time. It affects every command, not just the image ones.What changes, measured against the live API
Each row is the same command run against
main's build and this branch's build:repository gh claudiacodacy no-such-repoError: Not FoundError: Could not find repository gh/claudiacodacy/no-such-repo (HTTP 404)repositories gh no-such-orgError: Not FoundError: Could not find organization for provider: gh, organizationName: no-such-org (HTTP 404)pull-request … 99999Error: Not FoundError: Cannot find pull request with number '99999' (HTTP 404)finding gh claudiacodacy 0000…Error: Not FoundError: Item with id 0000… not found (HTTP 404)infowith a bad tokenError: UnauthorizedError: Bad credentials (HTTP 401)image … --tag 9.9.9 --upload …Error: Bad RequestError: SBOM tag mismatch: expected 9.9.9, found 3.20 (HTTP 400)issues,ls,directoriesandimagesall improve the same way.tool,patternsandimage <missing>are unchanged — they handle those cases themselves and never reachhandleError.Decisions
(HTTP 404). It is the part that gets quoted in a bug report, andUnauthorizedcarries meaning that a terse body message might not.import-config.tsalready hadparseApiErrorBodydoing exactly this fortools --import's failure table — good evidence the fix belongs to us, not the API. It is nowapiErrorDetailsinutils/error.ts, used by both, so a new body shape has one place to be handled. It reads the spec'smessage, an out-of-specerrorsarray, a plain string body, else the serialized body.Tests
13 new in
src/utils/error.test.ts(the file didn't exist), 734 passing, no existing test touched. Plus the live before/after sweep above across 11 commands.🤖 Generated with Claude Code