Repository navigation
fix: stream binary download responses - #108
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the CLI’s binary download handling to stream response bodies directly to their destinations (stdout or explicit output files) instead of buffering entire responses in memory, while preserving the existing auto-detection behavior for terminal output via a temporary spool.
Changes:
- Stream response bodies with
io.Copyfor forced-stdout and explicit--outputfile paths. - Add
writeAutomaticBinaryResponseto preserve full-response UTF-8 validation without in-memory buffering, using a private temporary spool. - Add extensive regression tests for large/chunked/unbounded responses, cancellation, short writes, temporary file privacy, and generated download endpoints.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| pkg/cmd/cmdutil.go | Switches binary response handling to streaming; adds temp-spool-based automatic (terminal) output behavior. |
| pkg/cmd/binaryresponse_test.go | Adds regression tests covering streaming, chunked responses, cancellation, writer failures, and generated download commands. |
Suppressed comments (1)
pkg/cmd/cmdutil.go:293
writeAutomaticBinaryResponsewrites to the created download file but only defersfile.Close(), so any close-time error (flush/sync failure) is silently ignored. Since this code is explicitly dealing with large/binary downloads, it’s important to surface close failures just like the explicit-output branch does.
Close the file explicitly after io.Copy, and avoid the deferred close so the error can be returned.
file, err := createDownloadFile(response, sample)
if err != nil {
return "", err
}
defer file.Close()
if _, err := io.Copy(file, spool); err != nil {
return "", err
}
return fmt.Sprintf("Wrote output to: %s", file.Name()), nil
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (2)
pkg/cmd/cmdutil.go:251
writeAutomaticBinaryResponsevalidates UTF-8 by iterating rune-by-rune viabufio.Reader.ReadRune(). For large text downloads this adds significant per-rune overhead compared to validating in byte chunks (with a small carry buffer for split runes), while still requiring a full pass over the file. Consider switching to a chunked UTF-8 validator to reduce CPU time on large terminal downloads (the main case where this path is exercised).
r, size, err := reader.ReadRune()
pkg/cmd/binaryresponse_test.go:73
- This test is gated on
isTerminal(os.Stdout)and will be skipped in most CI environments wherego testruns with non-TTY stdout, so it won’t provide regression coverage there. Consider making the terminal/non-terminal decision injectable (or using a PTY in the test) so the terminal auto-download path is exercised deterministically in CI.
if !isTerminal(os.Stdout) {
t.Skip("automatic terminal output requires an actual terminal")
}
dc7c02a to
bbd7865
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (2)
pkg/cmd/cmdutil.go:252
writeAutomaticBinaryResponsevalidates UTF-8 by scanning the entire spool rune-by-rune even when the MIME sniffed prefix is clearly not text (e.g., PDFs/PNGs). In the terminal-auto path this can add an extra full pass over large binary downloads (copy-to-spool + full UTF-8 scan + copy-to-destination), which is avoidable without changing behavior: only do the full-response UTF-8 scan (and the invalid-after-sample adjustment) when the sampled prefix is classified as text byisUTF8TextFile(sample).
validUTF8 := true
reader := bufio.NewReader(spool)
for {
r, size, err := reader.ReadRune()
if errors.Is(err, io.EOF) {
pkg/cmd/binaryresponse_test.go:407
- The test server handler writes the incoming request into a
requestschannel with capacity 1. If the CLI ever performs more than one HTTP request for a given command (e.g., a metadata call plus the download), the second handler invocation will block onrequests <- requestand can deadlock/hang the test. Making the send non-blocking (and/or increasing the buffer) avoids a brittle test hang without changing what the test asserts (it still reads the first request).
requests := make(chan *http.Request, 1)
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
requests <- request
response.Header().Set("Content-Type", "application/octet-stream")
HAYDEN-OAI
left a comment
There was a problem hiding this comment.
Reviewed the exact head and found three actionable P2 regressions in download correctness, disk usage, and interruption cleanup.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b7f65e28f8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
pkg/cmd/cmdutil.go:251
- writeExplicitBinaryResponse always creates a staging temp file in filepath.Dir(target) and then renames it into place. This changes behavior for overwriting an existing regular file: the old os.WriteFile-based approach only needed write permission on the file itself, but the new transactional path also requires write permission on the containing directory (to create the temp file and perform the rename). That can cause unexpected EPERM even when the destination file is writable.
Consider falling back to an in-place stream overwrite when the destination exists and staging fails with a permission error (transactional replacement isn’t possible in that environment anyway).
staged, err := os.CreateTemp(filepath.Dir(target), ".openai-cli-download-*")
if err != nil {
return err
}
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dbd5e1a0da
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
dbd5e1a to
a09f0f1
Compare
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Castiron custom code✅ No new custom-code files detected. 5 mixed files remain; 0 existing customizations changed. Compared 5 existing customizations unchanged
A changed generated baseline means this report cannot reliably identify which handwritten lines changed. Inspect the custom-code diffDownload the exact patch produced by this run (requires repository access): gh run download 32622121073 --repo openai/openai-cli \
--name castiron-custom-code-32622121073-1 --dir /tmp/castiron-custom-code-32622121073-1
git apply --stat /tmp/castiron-custom-code-32622121073-1/custom-code.patch
cat /tmp/castiron-custom-code-32622121073-1/custom-code.patchOr reproduce it from an SDK checkout containing the vendored reporter: git fetch --no-tags origin 681d595d302ffe9db8ec6a7efbd32c2762785ce1 8362baf1bc5e132078ac1f21f027ae9883ae5a75
python3 scripts/castiron/custom_code_report.py report \
--base 681d595d302ffe9db8ec6a7efbd32c2762785ce1 \
--head 8362baf1bc5e132078ac1f21f027ae9883ae5a75 --fetch --require-head-hash --public \
--out /tmp/castiron-custom-code-8362baf1bc5e
cat /tmp/castiron-custom-code-8362baf1bc5e/custom-code.patchThis is the current full custom patch for mixed files, not an attribution of only the handwritten lines changed by this PR. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a09f0f1266
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
c5526e0 to
f1228dd
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1228ddc69
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9800c8c316
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8362baf1bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
apcha-oai
left a comment
There was a problem hiding this comment.
[sdk codex review] Completed two independent full reviews of all three changed files, six download callers and all 33 existing review threads at 8362baf1bc5e132078ac1f21f027ae9883ae5a75. No substantiated new blocking issue within the author's declared direct-streaming/bounded-prefix design; no duplicate inline comments.
The tradeoffs remain explicit: automatic routing samples up to 515 bytes, later nontext bytes can follow a text-looking prefix, explicit outputs are truncated before completion, and failed downloads can retain partial files. The removed spool/promotion/unlink architectures are not present. Separate human acceptance of these tradeoffs was not independently verified; the PR summary's spool description should also be aligned with current source.
Verified CI run 32608925033/test job 97118546458 checked out merge ba9f471 of this exact head into current main 681d595. Go 1.25.14 Linux package tests passed via ./scripts/test without skip arguments, followed by Windows test compilation. CI, CodeQL and trusted Castiron checks report success. Nonverbose logs do not prove execution of the conditional real-TTY test; Windows compilation is not Windows runtime testing. No new tests, builds or security scans were run for this review.
This completed automated review COMMENT does not provide the requested human SDK CODEOWNER approval. Human signoff remains outstanding. Head, base/merge-base, author, open/non-draft status, mergeability, checks and feedback were verified before submission.
|
Updated the PR Summary and Validation to match the approved final direct-streaming/bounded-prefix implementation, its explicit compatibility tradeoffs, current regression coverage, and passing hosted checks, as requested in #108 (review). No source or commit changes. |
HAYDEN-OAI
left a comment
There was a problem hiding this comment.
Reviewed the exact head, including direct streaming across all binary-download callers, bounded-prefix terminal routing, response/body ownership, cancellation and short-write propagation, destination permissions and identity, and the documented direct-streaming tradeoffs. No new substantive correctness, security, or compatibility issues found.
Automated Release PR --- ## [1.9.0](openai/openai-cli@v1.8.0...v1.9.0) (2026-08-26) ### Features * **api:** Add residency flag and deprecate geography ([openai#136](openai#136)) ([e460582](openai@e460582)) ### Bug Fixes * Add provenance-aware untrusted stdin mode ([openai#137](openai#137)) ([a133707](openai@a133707)) * build CLI releases with patched Go toolchains ([openai#93](openai#93)) ([a58f1cc](openai@a58f1cc)) * discard fish completion diagnostics ([openai#109](openai#109)) ([4f35fc0](openai@4f35fc0)) * escape raw output controls on interactive terminals ([openai#112](openai#112)) ([7601afb](openai@7601afb)) * fail closed when bootstrapping Go dependencies ([openai#94](openai#94)) ([4ebe610](openai@4ebe610)) * lock and verify Steady mock tooling ([openai#95](openai#95)) ([681d595](openai@681d595)) * prepare release artifacts before publishing ([openai#111](openai#111)) ([4193619](openai@4193619)) * redact sensitive response headers in debug logs ([openai#106](openai#106)) ([4931572](openai@4931572)) * reject invalid multipart MIME header controls ([openai#105](openai#105)) ([fad98b4](openai@fad98b4)) * restrict downloaded file permissions ([openai#107](openai#107)) ([7a3a7d1](openai@7a3a7d1)) * safely construct bash file completions ([openai#110](openai#110)) ([b3e0946](openai@b3e0946)) * stream binary download responses ([openai#108](openai#108)) ([1f3ba6c](openai@1f3ba6c)) * stream multipart uploads without buffering ([openai#65](openai#65)) ([bcf0ecd](openai@bcf0ecd)) * verify GoReleaser before privileged release execution ([openai#96](openai#96)) ([b3729ea](openai@b3729ea)) ### Chores * **api:** clarify image background and audio delta docs ([openai#124](openai#124)) ([056f306](openai@056f306)) * **api:** No customer-visible SDK or public API changes ([openai#128](openai#128)) ([12e0d28](openai@12e0d28)) * **api:** refresh image model API reference ([openai#115](openai#115)) ([a771913](openai@a771913)) * **api:** Refresh the bundled OpenAPI reference ([openai#66](openai#66)) ([d14387a](openai@d14387a)) * **api:** Report no customer-visible SDK or public API changes ([openai#113](openai#113)) ([8da2d53](openai@8da2d53)) * **api:** update generation metadata only ([openai#138](openai#138)) ([4244a62](openai@4244a62)) * configure Dependabot for Go and GitHub Actions ([openai#69](openai#69)) ([f495366](openai@f495366)) * **deps:** bump actions/attest-build-provenance from 4.1.0 to 4.2.2 ([openai#102](openai#102)) ([6594c73](openai@6594c73)) * **deps:** bump actions/checkout from 6.0.2 to 7.0.1 ([openai#103](openai#103)) ([ce6a6d0](openai@ce6a6d0)) * **deps:** bump actions/checkout from 6.0.2 to 7.0.1 ([openai#134](openai#134)) ([f40d5f2](openai@f40d5f2)) * **deps:** bump actions/download-artifact from 4.3.0 to 8.0.1 ([openai#133](openai#133)) ([98ed029](openai@98ed029)) * **deps:** bump actions/github-script from 7.1.0 to 9.0.0 ([openai#132](openai#132)) ([978ce47](openai@978ce47)) * **deps:** bump actions/setup-go in / ([openai#101](openai#101)) ([5564982](openai@5564982)) * **deps:** bump actions/upload-artifact from 4.6.2 to 7.0.1 ([openai#104](openai#104)) ([b3ac15a](openai@b3ac15a)) * **deps:** bump github.com/charmbracelet/bubbles from 0.21.0 to 1.0.0 ([openai#99](openai#99)) ([7ac7035](openai@7ac7035)) * **deps:** bump github.com/urfave/cli/v3 from 3.4.1 to 3.10.1 in the go-minor-and-patch group across 1 directory ([openai#130](openai#130)) ([ddc612e](openai@ddc612e)) * **deps:** bump the charmbracelet group across 1 directory with 2 updates ([openai#97](openai#97)) ([2137b80](openai@2137b80)) * **deps:** bump the codeql group across 1 directory with 2 updates ([openai#100](openai#100)) ([c95ac0a](openai@c95ac0a)) * **deps:** bump the codeql group across 1 directory with 2 updates ([openai#131](openai#131)) ([58e3bdd](openai@58e3bdd)) * **deps:** bump the go-minor-and-patch group across 1 directory with 5 updates ([openai#98](openai#98)) ([74822be](openai@74822be)) * **deps:** update openai-go to v3.51.0 ([openai#63](openai#63)) ([68e3e70](openai@68e3e70)) * **deps:** update openai-go to v3.52.0 ([openai#67](openai#67)) ([7d87ee2](openai@7d87ee2)) * set a 1,000-line custom-code budget ([openai#126](openai#126)) ([016bfb2](openai@016bfb2)) ### Documentation * add secure CLI agent and contributor guidance ([openai#72](openai#72)) ([09ec087](openai@09ec087)) * standardize CLI vulnerability disclosure policy ([openai#71](openai#71)) ([d082a01](openai@d082a01)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: openai-sdks[bot] <284451331+openai-sdks[bot]@users.noreply.github.com>
Summary
Validation
./scripts/lint,go vet ./...,go mod verify, Castiron checker tests, and Linux/Windows/macOS cross-platform build checks passed.