Skip to content

fix(http): EOF downloads for VSCode CLI#725

Merged
skevetter merged 17 commits into
mainfrom
fix/vscode-cli-download-retry
Jul 24, 2026
Merged

fix(http): EOF downloads for VSCode CLI#725
skevetter merged 17 commits into
mainfrom
fix/vscode-cli-download-retry

Conversation

@skevetter

@skevetter skevetter commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

HTTP download hardening for devsy up: fixes flaky VS Code CLI failures, extracts the download mechanics into a reusable abstraction, and closes a TLS-verification hole in the shared client.

1. VS Code CLI unexpected EOF (root cause)

vscodeweb streamed the HTTP body straight into the extractor with only a status check. A truncated transfer still returns 200; the short body only failed inside gzip/tar as unexpected EOF — misattributed to extraction, with no retry, so one hiccup failed the whole up.

2. New devsyhttp.DownloadToFile abstraction

DownloadToFile(ctx, url, destPath, opts...):

  • Functional options: WithHeaders, WithBackoff, WithClient.
  • Transient vs permanent classification: connection errors, 5xx, and truncated transfers retry with exponential backoff; 4xx fails fast.
  • Completeness check against Content-Length → clear incomplete transfer, got X of Y bytes instead of a later decode failure.
  • Truncates the destination each attempt so a partial download never leaks forward.

Why not in the shared *http.Client: a truncated body isn't visible at RoundTrip time (it happens while the caller streams resp.Body), so a retrying transport can't catch it without buffering every response for the singleton client every caller shares. Completeness is a download-helper concern; domain integrity (sha256/OCI digest) stays with the caller that knows the expected value.

Both duplicated paths moved onto it:

  • vscodeweb.downloadAndExtract delegates download (retry + completeness), owns only extraction — an extract failure now means a genuinely corrupt archive.
  • feature.downloadFeatureFromURL drops its hand-rolled retry loop and tryDownload.

3. TLS verification on by default

The shared client set InsecureSkipVerify unconditionally, disabling certificate verification for every caller — including IDE/agent binary downloads from public CDNs (MITM exposure). The genuinely-insecure paths (self-signed pro/platform, in-container setup) already use their own dedicated insecure transports, and the localhost credential flows are plain HTTP, so the shared client never needed it. Now verifies by default; insecurity is opt-in via DEVSY_INSECURE_TLS for environments that must reach self-signed hosts.

Tests

  • pkg/http: download success, headers, truncation-retry, transient recovery, 4xx fail-fast, 5xx retry; insecureTLSEnabled default-secure / opt-in matrix.
  • pkg/ide/vscodeweb: download+extract success, and a complete-but-corrupt archive reported as an extract error.

Summary by CodeRabbit

  • New Features
    • Added a shared URL-to-file downloader with configurable headers, optional progress, stall detection, exponential-backoff retries, and “skip if same size” behavior.
    • Added shared HTTP retry behavior (including Retry-After) plus request user-agent injection and stall detection.
    • Added a download-and-extract installer that stages changes before swapping into place.
    • Added insecure TLS toggle via DEVSY_INSECURE_TLS (logs a warning when enabled).
  • Bug Fixes
    • Prevents partial/corrupt artifacts from being left behind and preserves existing install contents on failures, including proper context-cancellation handling.
  • Tests
    • Expanded coverage for downloader retry/truncation/stall/cancellation, HTTP retry/stall/user-agent, and installer staging/cleanup/permissions.

The VS Code CLI was streamed straight from the HTTP response body into the
extractor with only a status-code check, so a truncated transfer surfaced as
an opaque 'extract VS Code CLI: decompress: ... unexpected EOF' and failed the
whole up with no retry.

Download to a temp file first and verify the transfer completed (Content-Length
when advertised), so an incomplete download is reported as a clear, correctly
attributed 'download VS Code CLI: incomplete transfer' error, and an extract
failure now genuinely means a corrupt archive. Wrap the download+extract in
wait.ExponentialBackoff (matching oci_retry.go in the feature package) so a
transient CDN hiccup no longer fails the build.

Adds unit tests for success, truncation attribution, error status, and retry
recovery.
@netlify

netlify Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploy Preview for devsydev canceled.

Name Link
🔨 Latest commit 11fa6e3
🔍 Latest deploy log https://app.netlify.com/projects/devsydev/deploys/6a62b3da289f74000854bdbc

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: edae506a-489c-404b-9296-3995d132e4a8

📥 Commits

Reviewing files that changed from the base of the PR and between 297d417 and 11fa6e3.

📒 Files selected for processing (3)
  • pkg/http/download.go
  • pkg/http/download_test.go
  • pkg/http/http.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/http/download_test.go
  • pkg/http/download.go

📝 Walkthrough

Walkthrough

Adds shared HTTP download, retry, stall-detection, and archive-extraction utilities. Devcontainer features and IDE installers now use these utilities with configurable TLS, headers, caching, progress reporting, file modes, and staged installation replacement.

Changes

Download infrastructure

Layer / File(s) Summary
Configurable TLS and HTTP client
pkg/config/env.go, pkg/http/http.go, pkg/http/retry.go, pkg/http/useragent.go, pkg/http/http_test.go, pkg/http/retry_test.go, pkg/http/useragent_test.go
Adds configurable insecure TLS, retry transport behavior, stall-aware shared HTTP clients, and user-agent injection with tests.
Retrying and stall-aware file downloader
pkg/http/download.go, pkg/http/stall.go, pkg/http/download_test.go, pkg/http/stall_test.go
Adds configurable downloads with retries, status handling, atomic writes, progress wrapping, same-size skipping, stall detection, and content-length validation.
Archive download and extraction
pkg/ide/download.go, pkg/ide/download_test.go
Adds temporary archive downloads, staged extraction, atomic installation replacement, cleanup, and extraction workflow tests.
Direct-download integrations
pkg/devcontainer/feature/features.go, pkg/ide/fleet/fleet.go, pkg/ide/jetbrains/generic.go, pkg/ide/rstudio/rstudio.go
Migrates feature and IDE binary downloads to DownloadToFile with headers, modes, caching, and progress options.
Archive-install integrations
pkg/ide/codeserver/codeserver.go, pkg/ide/openvscode/openvscode.go, pkg/ide/vscodeweb/vscodeweb.go
Migrates archive installations to DownloadAndExtract and removes duplicated local download helpers.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Installer
  participant DownloadAndExtract
  participant DownloadToFile
  participant ArchiveStaging
  Installer->>DownloadAndExtract: request archive installation
  DownloadAndExtract->>DownloadToFile: download archive to temporary file
  DownloadToFile-->>DownloadAndExtract: completed archive
  DownloadAndExtract->>ArchiveStaging: extract and stage files
  ArchiveStaging-->>DownloadAndExtract: extraction result
  DownloadAndExtract-->>Installer: installed destination or error
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is related to the changeset because VS Code CLI downloads were updated and EOF/truncated-download handling is part of the work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@netlify

netlify Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploy Preview for images-devsy-sh canceled.

Name Link
🔨 Latest commit 11fa6e3
🔍 Latest deploy log https://app.netlify.com/projects/images-devsy-sh/deploys/6a62b3dbad370f000833bb03

Introduce devsyhttp.DownloadToFile: a reusable download abstraction with
functional options (WithHeaders/WithBackoff/WithClient), context support,
transient-vs-permanent error classification (4xx fails fast, 5xx/conn/
truncation retried), Content-Length completeness verification, and fresh
truncation of the destination on each retry.

Move both duplicated download paths onto it:
- vscodeweb.downloadAndExtract now delegates the download (retry + completeness)
  to the helper and only owns extraction, so an extract failure means a
  genuinely corrupt archive.
- feature.downloadFeatureFromURL drops its hand-rolled retry loop and
  tryDownload in favor of the helper.

Download completeness (Content-Length) lives in the helper; domain integrity
(sha256/OCI digest) stays with the caller that knows the expected value.

Tests for the helper cover success, headers, truncation retry, transient
recovery, 4xx fail-fast, and 5xx retry.
@skevetter skevetter changed the title fix(vscodeweb): retry VS Code CLI download and report truncation clearly fix(http): shared DownloadToFile helper with retry + completeness; fix VS Code CLI EOF Jul 23, 2026
The shared HTTP client set InsecureSkipVerify unconditionally, disabling TLS
certificate verification for every caller — including IDE/agent binary
downloads from public CDNs, exposing them to MITM. Genuinely-insecure paths
(self-signed pro/platform instances, in-container setup) already use their own
dedicated insecure transports, and the localhost credential flows are plain
HTTP, so the shared client does not need it.

Verify certificates by default and disable only via the DEVSY_INSECURE_TLS
opt-in escape hatch for environments that must reach self-signed hosts.
@github-actions github-actions Bot added size/xl and removed size/l labels Jul 23, 2026
Migrate every download-to-file site onto the shared helper, giving each the
retry + completeness verification it lacked:

- openvscode, codeserver: previously streamed the response body straight into
  the extractor (same truncation-EOF bug as vscode). Now download to a temp
  file first, then extract.
- fleet: download the binary via the helper with WithMode(0o755).
- rstudio, jetbrains: download the archive via the helper with SkipIfSameSize
  (preserving the existing size-based cache reuse) and WithProgress (preserving
  the progress log), dropping their bespoke fetch/save helpers.

Extends DownloadToFile with WithMode, WithProgress, and SkipIfSameSize options.

Left alone (not download-to-file): pkg/download (stream-returning API with
GitHub private-release auth), pkg/agent/binary (streaming tee-to-cache), and
the API/version/ping/POST callers.
…ition

The download-to-temp-file then extract dance was duplicated across vscodeweb,
openvscode, and codeserver. Extract it into ide.DownloadAndExtract, which
composes devsyhttp.DownloadToFile (transport + retry + completeness) with
extract.Extract (archive), passing through extract options (e.g. StripLevels)
and cleaning up a partial destination on failure.

Deliberately placed in pkg/ide rather than the http client: composing HTTP
transport with archive extraction belongs in the layer above both, keeping
pkg/http and pkg/extract focused (neither imports the other or pkg/ide). All
three IDE installers already depend on pkg/ide.

Tests move with the logic into pkg/ide (success, corrupt-archive attribution,
cleanup on failure).
@skevetter
skevetter marked this pull request as ready for review July 23, 2026 06:29
@skevetter skevetter changed the title fix(http): shared DownloadToFile helper with retry + completeness; fix VS Code CLI EOF fix(http): EOF downloads for VSCode CLI Jul 23, 2026

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/ide/fleet/fleet.go (1)

64-91: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove truncated Fleet files before retrying the install.

DownloadToFile does not delete destPath when all retries or StartBackgroundOnce fail; it can leave a partial fleetBinary because retry attempts write with O_TRUNC to the final destination. Since Install() only uses os.Stat(fleetBinary) to detect an existing installation, a partial file from a failed previous run will make the next run try to execute and start a corrupt binary. Return on download error, but remove/reinstate the file if it no longer exists or is invalid before starting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ide/fleet/fleet.go` around lines 64 - 91, Update FleetServer.Install
around the devsyhttp.DownloadToFile call to remove fleetBinary when downloading
fails, before returning the error, so truncated files cannot be mistaken for an
installed binary. Also validate the existing fleetBinary detected by os.Stat
before calling Start, removing it when missing or invalid and continuing through
the download path.
🧹 Nitpick comments (1)
pkg/devcontainer/feature/features.go (1)

548-554: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Propagate a bounded context into DownloadToFile callers. These downloads use context.Background() from the caller all the way into http.NewRequestWithContext, while GetHTTPClient() does not set an http.Client.Timeout, so an unresponsive endpoint can block an in-flight download and stall the caller indefinitely. Thread a caller-owned context with a deadline into the integration wrapper functions instead of context.Background().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/devcontainer/feature/features.go` around lines 548 - 554, Replace
context.Background() with caller-owned, deadline-bounded contexts in the
DownloadToFile wrapper flows, threading each context through the relevant
integration functions. Update pkg/devcontainer/feature/features.go (the feature
download function), pkg/ide/fleet/fleet.go (the Fleet download caller),
pkg/ide/jetbrains/generic.go (the JetBrains download caller), and
pkg/ide/rstudio/rstudio.go (the RStudio download caller); preserve existing
download behavior while ensuring every request has a finite deadline.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/http/download.go`:
- Around line 139-164: The copyToFile function must write downloads atomically:
create a temporary file in destPath’s directory, apply cfg.mode, copy and close
the download, verify the content length, then rename the temp file to destPath
only on success. Ensure all failure paths remove the temporary file, and
preserve cfg.mode even when destPath already exists.

In `@pkg/ide/download.go`:
- Around line 39-42: Update the extraction flow in the download function
containing extract.Extract so archives are unpacked into a sibling staging
directory rather than the caller-provided destDir, and only replace destDir
after extraction succeeds. On extraction failure, clean up staging without
removing pre-existing destination contents, and add a regression test that seeds
destDir before downloading an invalid archive and verifies it remains intact.

---

Outside diff comments:
In `@pkg/ide/fleet/fleet.go`:
- Around line 64-91: Update FleetServer.Install around the
devsyhttp.DownloadToFile call to remove fleetBinary when downloading fails,
before returning the error, so truncated files cannot be mistaken for an
installed binary. Also validate the existing fleetBinary detected by os.Stat
before calling Start, removing it when missing or invalid and continuing through
the download path.

---

Nitpick comments:
In `@pkg/devcontainer/feature/features.go`:
- Around line 548-554: Replace context.Background() with caller-owned,
deadline-bounded contexts in the DownloadToFile wrapper flows, threading each
context through the relevant integration functions. Update
pkg/devcontainer/feature/features.go (the feature download function),
pkg/ide/fleet/fleet.go (the Fleet download caller), pkg/ide/jetbrains/generic.go
(the JetBrains download caller), and pkg/ide/rstudio/rstudio.go (the RStudio
download caller); preserve existing download behavior while ensuring every
request has a finite deadline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fb61b84-a3da-4bea-a3ed-4deed7f6eac5

📥 Commits

Reviewing files that changed from the base of the PR and between 068454e and c993030.

📒 Files selected for processing (14)
  • pkg/config/env.go
  • pkg/devcontainer/feature/features.go
  • pkg/http/download.go
  • pkg/http/download_test.go
  • pkg/http/http.go
  • pkg/http/http_test.go
  • pkg/ide/codeserver/codeserver.go
  • pkg/ide/download.go
  • pkg/ide/download_test.go
  • pkg/ide/fleet/fleet.go
  • pkg/ide/jetbrains/generic.go
  • pkg/ide/openvscode/openvscode.go
  • pkg/ide/rstudio/rstudio.go
  • pkg/ide/vscodeweb/vscodeweb.go

Comment thread pkg/http/download.go Outdated
Comment thread pkg/ide/download.go Outdated
- copyToFile writes to a temp file and renames on success, so a failed
  transfer never leaves a partial file at the destination (fixes callers
  like fleet that detect installs via os.Stat existence).
- DownloadAndExtract extracts into a staging dir and swaps it into place
  only on success, preserving a pre-existing install if extraction fails.
@skevetter
skevetter marked this pull request as draft July 23, 2026 20:18
The shared HTTP client sets no Client.Timeout and download callers pass
context.Background(), so an unresponsive endpoint could block a transfer
indefinitely. Add a per-attempt stall timeout (default 60s) that cancels
an attempt when no response headers arrive or no bytes are read within
the window, while a steadily-progressing transfer keeps rescheduling the
deadline. Stalls surface as retryable errors.
- Split copyToFile and DownloadAndExtract into smaller helpers to stay
  under the cyclomatic-complexity limit.
- Group per-attempt download state into a struct so helpers stay within
  the 4-argument limit.
- Use 0o750 dir / 0o600 file perms in tests and annotate the IDE install
  dir MkdirAll and test ReadFile for gosec.
@skevetter
skevetter marked this pull request as ready for review July 23, 2026 21:19

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/http/download.go`:
- Around line 181-187: Update downloadOnce’s HTTP status handling so 429 Too
Many Requests does not return permanentError and instead follows the retryable
error path used for 5xx responses. Adjust the nearby documentation describing
permanent 4xx statuses to explicitly exclude retryable client statuses such as
429, while preserving permanent handling for other applicable 4xx responses.

In `@pkg/ide/download.go`:
- Around line 66-82: Update the staging/install flow used by swapIntoPlace so
the directory installed at destDir has mode 0755 after extraction and rename.
Normalize permissions after the swap succeeds, or normalize stagingDir before
renaming while preserving cleanup and error propagation for chmod failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 39a7c8ec-6b00-488f-8fbf-855a210d9c51

📥 Commits

Reviewing files that changed from the base of the PR and between c993030 and fb672bc.

📒 Files selected for processing (4)
  • pkg/http/download.go
  • pkg/http/download_test.go
  • pkg/ide/download.go
  • pkg/ide/download_test.go

Comment thread pkg/http/download.go
Comment thread pkg/ide/download.go
@skevetter
skevetter marked this pull request as draft July 23, 2026 21:33
- Treat 429 Too Many Requests as a retryable status instead of a
  permanent 4xx failure, matching its retry-later semantics.
- Restore the 0755 install-dir mode: os.MkdirTemp creates the staging
  dir as 0700, so chmod it back to the mode extract.Extract previously
  produced for destDir before swapping it into place.
@skevetter
skevetter marked this pull request as ready for review July 23, 2026 22:33
@skevetter
skevetter marked this pull request as draft July 23, 2026 22:44
The inactivity timeout is a cross-cutting transport concern, so express it
as an http.RoundTripper (StallTransport) composed onto a download-scoped
client rather than threading a watchdog through the download loop. The
download algorithm no longer knows about stalls; it just sees a retryable
ErrStalled. Kept opt-in per client so the shared client and long-poll
callers are unaffected.
Restructure the shared HTTP client as a base transport plus composable
RoundTripper decorators:
- baseRoundTripper: TLS/proxy/dial + ResponseHeaderTimeout (bounds the
  header wait for all callers).
- WithUserAgent: sends devsy/<version> when the caller sets none.
- RetryTransport: retries idempotent (GET/HEAD) requests on connection
  errors, 5xx and 429 with exponential backoff, honoring Retry-After.
  Non-idempotent methods and caller-cancelled requests are never retried,
  so it composes safely with existing app-level retry loops.

GetHTTPClient now composes base + user-agent + retry. The download client
composes base + user-agent + stall (no retry — DownloadToFile owns
download retries, including truncation, so layering retry would
double-retry).
Retry only idempotent, bodyless requests — a consumed body cannot be
replayed. Add redirect and body-guard regression tests.
- swapIntoPlace: if restore also fails after a failed swap, return an
  error naming the preserved backup path instead of silently warning, so
  a rare double-rename failure cannot lose the install.
- RetryTransport.backoff: clamp shift overflow to MaxDelay.
@skevetter
skevetter marked this pull request as ready for review July 23, 2026 23:57

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/http/download.go (1)

113-134: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Propagate context cancellation errors instead of returning the last transient download error

wait.Interrupted(err) matches both step exhaustion and context cancellations/deadlines. Since the condition runs before each backoff sleep, a cancellation during backoff can leave lastErr holding the previous transient error, which breaks caller cancellation paths such as errors.Is(err, context.Canceled) and shutdown behavior.

🐛 Proposed fix
 	if wait.Interrupted(err) {
+		if ctxErr := ctx.Err(); ctxErr != nil {
+			return ctxErr
+		}
 		return lastErr
 	}
 	return err
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/http/download.go` around lines 113 - 134, Update the retry result
handling after wait.ExponentialBackoffWithContext in the download flow: when
wait.Interrupted(err) indicates context cancellation or deadline expiration,
return the context error from ctx rather than the stale lastErr. Preserve
returning lastErr for ordinary backoff exhaustion and keep direct propagation of
non-interrupted errors unchanged.
🧹 Nitpick comments (1)
pkg/http/http.go (1)

41-50: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

GetHTTPClient() has no overall request timeout.

ResponseHeaderTimeout only bounds the wait for response headers; once headers arrive, a slow/stalled body read on this general-purpose shared client can block the caller's goroutine indefinitely (no Client.Timeout, no stall guard like the download client has). Since this is the package's general shared client, consider setting a bounded Timeout (or clearly documenting that every caller must supply a context deadline).

♻️ Proposed fix
 func GetHTTPClient() *http.Client {
 	httpClientOnce.Do(func() {
 		rt := WithUserAgent(baseRoundTripper(), UserAgent())
 		rt = NewRetryTransport(rt, DefaultRetry)
-		httpClientInst = &http.Client{Transport: rt}
+		httpClientInst = &http.Client{Transport: rt, Timeout: 2 * time.Minute}
 	})
 	return httpClientInst
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/http/http.go` around lines 41 - 50, Update GetHTTPClient to configure a
bounded overall request timeout on the shared http.Client, covering both
response-header waits and stalled body reads. Reuse the package’s established
timeout constant or configuration if available, while preserving the existing
transport and retry setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/http/http.go`:
- Around line 18-24: Update the transport caching around baseTransportOnce and
baseRoundTripper so changes to DEVSY_INSECURE_TLS are reflected after
initialization. Rebuild the cached base transport when the environment setting
changes, or provide a documented reset mechanism that tests and integrations can
invoke; ensure GetHTTPClient uses the transport matching the current TLS mode.
- Around line 26-39: Remove the fixed ResponseHeaderTimeout from the transport
returned by baseRoundTripper, or otherwise ensure download clients created
through WithStallTimeout use a transport whose header timeout is not shorter
than the configured stall timeout. Update both pkg/http/http.go at lines 26-39
and pkg/http/download.go at lines 36-42, preserving StallTransport as the
effective timeout for waits longer than 60 seconds.

---

Outside diff comments:
In `@pkg/http/download.go`:
- Around line 113-134: Update the retry result handling after
wait.ExponentialBackoffWithContext in the download flow: when
wait.Interrupted(err) indicates context cancellation or deadline expiration,
return the context error from ctx rather than the stale lastErr. Preserve
returning lastErr for ordinary backoff exhaustion and keep direct propagation of
non-interrupted errors unchanged.

---

Nitpick comments:
In `@pkg/http/http.go`:
- Around line 41-50: Update GetHTTPClient to configure a bounded overall request
timeout on the shared http.Client, covering both response-header waits and
stalled body reads. Reuse the package’s established timeout constant or
configuration if available, while preserving the existing transport and retry
setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 47dc8f4e-3112-416e-9c86-3bbaf15c3bb2

📥 Commits

Reviewing files that changed from the base of the PR and between 74ec75e and 297d417.

📒 Files selected for processing (9)
  • pkg/http/download.go
  • pkg/http/http.go
  • pkg/http/retry.go
  • pkg/http/retry_test.go
  • pkg/http/stall.go
  • pkg/http/stall_test.go
  • pkg/http/useragent.go
  • pkg/http/useragent_test.go
  • pkg/ide/download.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/ide/download.go

Comment thread pkg/http/http.go
Comment on lines 18 to 24
var (
httpClient *http.Client
baseTransportOnce sync.Once
baseTransportInst *http.Transport

httpClientOnce sync.Once
httpClientInst *http.Client
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether pkg/http/http_test.go resets the transport singleton between
# TLS-related test cases (e.g. via an exported reset helper or t.Setenv ordering).
rg -n "baseTransportOnce|httpClientOnce|DEVSY_INSECURE_TLS|t.Setenv" pkg/http/http_test.go

Repository: devsy-org/devsy

Length of output: 201


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
fd -a 'http.go|http_test.go' . | sed 's#^\./##'

echo "== package outline =="
ast-grep outline pkg/http/http.go || true
ast-grep outline pkg/http/http_test.go || true

echo "== relevant source =="
cat -n pkg/http/http.go

echo "== relevant tests =="
cat -n pkg/http/http_test.go

echo "== env constant references =="
rg -n "EnvInsecureTLS|DEVSY_INSECURE_TLS|baseTransportOnce|httpClientOnce|baseRoundTripper|GetHTTPClient" .

Repository: devsy-org/devsy

Length of output: 6726


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== reset/override occurrences =="
rg -n "Reset|ResetHTTP|baseTransportInst|BaseTransportInst|baseTransportOnce\." .

echo "== deterministic Go sync.Once behavior probe (using stdlib only) =="
cat > /tmp/syn_once_probe.go <<'EOF'
package main

import (
  "fmt"
  "os"
  "runtime"
  "sync"
  "strconv"
)

var (
  envOnce sync.Once
  gotValue bool
)

func getValue() string {
  if !gotValue {
    envOnce.Do(func() {
      gotValue = true
    })
  }
  return os.Getenv("TEST_VALUE")
}

func main() {
  got = "first"
  os.Setenv("TEST_VALUE", "first")
  v1 := getValue()
  os.Setenv("TEST_VALUE", "second")
  v2 := getValue()
  fmt.Printf("first=%q second=%q runtime=%s\n", v1, v2, runtime.Version())
}
EOF
go run /tmp/syn_once_probe.go

Repository: devsy-org/devsy

Length of output: 9870


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deterministic Go sync.Once behavior probe (fixed) =="
cat > /tmp/syn_once_probe.go <<'EOF'
package main

import (
  "fmt"
  "os"
  "runtime"
  "sync"
)

var (
  envChecked sync.Once
  checkedOnce bool
)

func getValue() string {
  envChecked.Do(func() { checkedOnce = true })
  return os.Getenv("TEST_VALUE")
}

func main() {
  os.Setenv("TEST_VALUE", "first")
  v1 := getValue()
  os.Setenv("TEST_VALUE", "second")
  v2 := getValue()
  fmt.Printf("first=%q second=%q checked_once=%v runtime=%s\n", v1, v2, checkedOnce, runtime.Version())
}
EOF
go run /tmp/syn_once_probe.go

echo "== reset helper for transport/http client existence =="
rg -n "Reset.*HTTP|ResetHTTP|baseTransportInst|httpClientInst|baseTransportOnce|httpClientOnce" pkg/http pkg/download pkg/credentials pkg/gitsshsigning || true

Repository: devsy-org/devsy

Length of output: 827


Don’t cache DEVSY_INSECURE_TLS behind sync.Once.

baseTransportOnce.Do reads the env var once and stores that transport forever, so changing the var after baseRoundTripper() or GetHTTPClient() has run does nothing. This makes the flag order-dependent and can silently produce insecure or secure clients in tests/integration workflows. Either make the TLS mode mutable/resettable with a documented reset helper, or rebuild the singleton when the setting changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/http/http.go` around lines 18 - 24, Update the transport caching around
baseTransportOnce and baseRoundTripper so changes to DEVSY_INSECURE_TLS are
reflected after initialization. Rebuild the cached base transport when the
environment setting changes, or provide a documented reset mechanism that tests
and integrations can invoke; ensure GetHTTPClient uses the transport matching
the current TLS mode.

Comment thread pkg/http/http.go
@skevetter
skevetter marked this pull request as draft July 24, 2026 00:14
- DownloadToFile: on interruption return the context error, not the stale
  last transient error, so callers can detect cancellation.
- GetHTTPClient: replace the base ResponseHeaderTimeout with a StallTransport
  (idle guard covering header and body) so a stalled body read cannot hang a
  caller, without a total timeout that would break large downloads. Removes
  the header-timeout/stall-timeout overlap.
@skevetter
skevetter marked this pull request as ready for review July 24, 2026 01:11
@skevetter
skevetter merged commit c07b339 into main Jul 24, 2026
64 checks passed
@skevetter
skevetter deleted the fix/vscode-cli-download-retry branch July 24, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant