Skip to content

fix: preserve binary response bytes in api command - #1502

Open
sbezludny wants to merge 2 commits into
mainfrom
codex/fix-api-binary-responses
Open

sbezludny wants to merge 2 commits into
mainfrom
codex/fix-api-binary-responses

Conversation

@sbezludny

@sbezludny sbezludny commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Affected Components

  • CLI
  • Create CLI
  • Test
  • Docs, in a separate PR
  • Examples
  • Other

Notes for the Reviewer

Downloading a ZIP through checkly api corrupts the archive when Axios decodes its bytes as UTF-8 and the command adds a newline. Request a response stream and write attachments and non-JSON bodies directly to stdout, waiting for each write before handling HTTP error exits. Redirected downloads retain their bytes too.

Buffer and parse responses declared as application/json or a +json media type, unless they are attachments. This preserves the formatting of ordinary API responses while downloaded JSON files retain their whitespace, numeric spelling, and large integers. Invalid JSON bodies retain their original bytes.

--jq explicitly requests JSON parsing regardless of content type. Non-JSON bodies fail with Response is not JSON; --jq cannot be applied before spawning jq. Output handling checks the parsed Buffer only once and no longer decodes non-JSON downloads to strings.

Documentation: checkly/docs#537. It adds a file-download example and describes the output behavior. There are no new flags or dependencies.

Validation:

  • Six new packaged-CLI cases fail on the previous PR commit for the expected reasons: two rewritten JSON downloads, three unclear --jq errors, and a download that produces no output before the response completes.
  • All 67 focused API command, response, field-parsing, and REST tests pass after the fixes. Real-jq success cases run when jq is installed; they all ran locally.
  • Full lint, TypeScript build, type tests, manifest generation, packing, and api --help pass. Commit hooks pass without being skipped.
  • The exact documented npx checkly api /path/to/file > report.zip command preserves the source ZIP bytes against a local server and passes unzip -t.
  • The docs frontmatter check and git diff --check pass. CI is pending.

@sbezludny
sbezludny requested a review from sorccu September 30, 2026 11:45

@martzoukos martzoukos 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.

I looked at the code and fought my agent suggesting you read the content-type instead (your JSON-read attempt is more solid in my opinion), and the verdict is 👍

@sorccu sorccu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, this fixes the corruption nicely. Nothing blocking from me, but a few points worth considering:

1. A downloaded file that happens to be valid JSON is still rewritten (parseResponse)
JSON detection goes by the body, not the Content-Type. A JSON file served as application/octet-stream (or as an attachment) is still re-serialized: whitespace and key formatting change, 1e3 becomes 1000, integers above 2^53 lose precision, and a trailing newline is added. The old behavior was the same, so this isn't a regression. It is the same "saved file ≠ server bytes" class of problem this PR fixes, though. Parsing only when the content type is JSON (application/json / +json), or when --jq is set, would close it.

2. Docs
Non-JSON bodies are now written unchanged without a trailing newline, and checkly api <url> > file.zip works. Neither is mentioned in docs/cli/checkly-api.mdx. A short "Download a file" example there (in the docs repo) would help people discover it.

3. Output branch could be simpler
Buffer.isBuffer(responseData) is checked twice, and the nested ternary only exists to decode a non-JSON Buffer to a string for --jq. applyJq could take string | Buffer instead, since child.stdin.write accepts both. This is optional cleanup.

4. --jq on a non-JSON response gives a confusing error
checkly api <zip-url> --jq . sends the body to jq, which fails with something like jq failed: jq: parse error: Invalid numeric literal at EOF at line 1, column 5. Note that (3) does not change this: jq reports the identical error for the raw bytes and for the U+FFFD-decoded string. A clearer fix would be to check before calling jq: if --jq is set and the response didn't parse as JSON, fail with a message like "Response is not JSON; --jq cannot be applied" and skip jq entirely.

5. Large downloads are fully buffered
With responseType: 'arraybuffer' the whole body is held in memory. For valid UTF-8 bodies a decoded string copy is also made to try JSON.parse, and nothing is written until the download completes. This was fine when checkly api only returned JSON API responses. Now that it's a supported way to download report and trace archives, responses can be large. The asset downloader (result-assets.ts) already streams with responseType: 'stream'. Streaming non-JSON responses straight to stdout, and only buffering when the content type is JSON (which ties in with 1), would keep memory flat. Not necessarily for this PR, but worth keeping in mind.

This branch has not been deployed

No deployments
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.

3 participants