Conversation
martzoukos
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
Affected Components
Notes for the Reviewer
Downloading a ZIP through
checkly apicorrupts 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/jsonor a+jsonmedia 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.--jqexplicitly requests JSON parsing regardless of content type. Non-JSON bodies fail withResponse is not JSON; --jq cannot be appliedbefore 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:
--jqerrors, and a download that produces no output before the response completes.api --helppass. Commit hooks pass without being skipped.npx checkly api /path/to/file > report.zipcommand preserves the source ZIP bytes against a local server and passesunzip -t.git diff --checkpass. CI is pending.