Skip to content

fix: harden unauthenticated GitHub API fallbacks with context and 30s timeout#42521

Merged
pelikhan merged 3 commits into
mainfrom
copilot/deep-report-harden-github-api-fallbacks
Jun 30, 2026
Merged

fix: harden unauthenticated GitHub API fallbacks with context and 30s timeout#42521
pelikhan merged 3 commits into
mainfrom
copilot/deep-report-harden-github-api-fallbacks

Conversation

Copilot AI commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Three unauthenticated api.github.com fallback paths dispatched via http.DefaultClient (no timeout), allowing a slow GitHub response to hang a goroutine indefinitely during workflow compilation. fetchPublicGitHubAPI already threaded context.Context but still used http.DefaultClient.

Changes

pkg/parser/remote_fetch.go

  • Adds package-level publicAPIClient = &http.Client{Timeout: constants.DefaultHTTPClientTimeout} (30s) shared across all public-API fallback calls.
  • resolveRefToSHAViaPublicAPI — adds ctx context.Context parameter; switches to http.NewRequestWithContext + publicAPIClient.
  • fetchPublicGitHubContentsAPI — same fix; threads ctx through all four immediate callers: downloadFileViaPublicAPI, listDirAllFilesViaPublicAPI, listDirSubdirsViaPublicAPI, listWorkflowFilesViaPublicAPI.
  • Call sites that have no ambient context pass context.Background(), matching the existing pattern already used in this file.

pkg/cli/update_workflows.go

  • Adds package-level updatePublicAPIClient = &http.Client{Timeout: constants.DefaultHTTPClientTimeout}.
  • fetchPublicGitHubAPI — replaces the lingering http.DefaultClient.Do(req) with updatePublicAPIClient.Do(req).
// Before
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
resp, err := http.DefaultClient.Do(req)  // no timeout

// After
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
resp, err := publicAPIClient.Do(req)     // 30s timeout

Copilot AI and others added 2 commits June 30, 2026 16:34
…eout

- resolveRefToSHAViaPublicAPI: add ctx parameter, use NewRequestWithContext + timeout client
- fetchPublicGitHubContentsAPI: add ctx parameter, use NewRequestWithContext + timeout client
- Thread ctx through immediate wrappers: downloadFileViaPublicAPI, listDirAllFilesViaPublicAPI, listDirSubdirsViaPublicAPI, listWorkflowFilesViaPublicAPI
- fetchPublicGitHubAPI: replace http.DefaultClient with &http.Client{Timeout: DefaultHTTPClientTimeout}

All callers that lack a context pass context.Background(), consistent with
the existing pattern (e.g. downloadFileViaGit calls on line 901/921).

Fixes #42500

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…backs

Avoid allocating a new http.Client on every call by introducing two
package-level variables:
- parser.publicAPIClient (remote_fetch.go) — shared by resolveRefToSHAViaPublicAPI and fetchPublicGitHubContentsAPI
- cli.updatePublicAPIClient (update_workflows.go) — used by fetchPublicGitHubAPI

Both carry DefaultHTTPClientTimeout (30s).

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Harden 3 unauthenticated GitHub API fallbacks with context and timeout fix: harden unauthenticated GitHub API fallbacks with context and 30s timeout Jun 30, 2026
Copilot AI requested a review from pelikhan June 30, 2026 16:38
@pelikhan pelikhan marked this pull request as ready for review June 30, 2026 17:26
Copilot AI review requested due to automatic review settings June 30, 2026 17:26
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (30 additions detected).

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. Test Quality Sentinel skipped.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens unauthenticated api.github.com fallback requests in gh-aw to avoid indefinite hangs during workflow compilation/update resolution by ensuring all such HTTP calls are context-aware and use a shared client with a 30s timeout.

Changes:

  • Introduces shared http.Client instances with constants.DefaultHTTPClientTimeout for public GitHub API fallbacks.
  • Threads context.Context into the parser’s public-API fallback helpers and switches requests to http.NewRequestWithContext.
  • Updates the update-workflows unauthenticated API path to stop using http.DefaultClient and instead use the timeout-configured client.
Show a summary per file
File Description
pkg/parser/remote_fetch.go Adds a shared timeout HTTP client and propagates context.Context through unauthenticated public-API fallback calls.
pkg/cli/update_workflows.go Replaces http.DefaultClient usage in unauthenticated fallback calls with a shared timeout HTTP client.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Low

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review summary

The core fix is correct and well-targeted. The two package-level HTTP clients (publicAPIClient in pkg/parser, updatePublicAPIClient in pkg/cli) both carry constants.DefaultHTTPClientTimeout (30 s), which is the right way to bound unauthenticated GitHub API calls that previously ran through http.DefaultClient with no timeout.

What the fix gets right:

  • All three previously-unbounded paths (resolveRefToSHAViaPublicAPI, fetchPublicGitHubContentsAPI, fetchPublicGitHubAPI) now have a hard 30 s timeout.
  • fetchPublicGitHubAPI already had ctx threaded but used http.DefaultClient — that oversight is now closed.
  • Switching to http.NewRequestWithContext is the correct API for context-aware requests.

Non-blocking observation (see inline comment):
All five call sites in pkg/parser/remote_fetch.go pass context.Background() because the intermediate callers (resolveRefToSHA, downloadFileFromGitHubWithDepth, etc.) have no ctx parameter. The ctx argument to http.NewRequestWithContext is therefore a no-op; the client-level timeout is the real hang protection. This is consistent with the pre-existing downloadFileViaGit(context.Background(), ...) pattern in the same file, but propagating ctx through those intermediate functions would be a valuable follow-up.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 42.3 AIC · ⌖ 6.75 AIC · ⊞ 4.9K

if host == "" || host == "github.com" {
remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s@%s", owner, repo, ref)
return resolveRefToSHAViaPublicAPI(owner, repo, ref)
return resolveRefToSHAViaPublicAPI(context.Background(), owner, repo, ref)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The five fallback call sites are all forced to use context.Background() because the intermediate callers (resolveRefToSHA, downloadFileFromGitHubWithDepth, listWorkflowFilesForHost, listDirAllFilesForHost, listDirSubdirsForHost) have no ctx parameter. The 30 s client-level timeout is the real hang protection here — the ctx passed to http.NewRequestWithContext carries no deadline or cancellation signal from the outer call stack. As a follow-up, propagating ctx through those intermediate functions would allow callers to cancel in-flight requests via their own context deadlines. @copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The 30 s timeout is real — the context threading is not

The http.Client{Timeout: 30s} additions in both files are a genuine improvement and the right direction. The updatePublicAPIClient fix in update_workflows.go is clean and complete.

The blocking concern is in remote_fetch.go: the ctx parameters were added to the five inner ViaPublicAPI functions, but all five outer callers (resolveRefToSHA, downloadFileFromGitHubWithDepth, listWorkflowFilesForHost, listDirAllFilesForHost, listDirSubdirsForHost) are context-free and pass context.Background(). The context refactor is scaffolding with no current effect on cancellation — a Ctrl-C or a caller deadline still can't interrupt these HTTP calls. Thread ctx through the outer functions to complete the fix.

Two secondary concerns are noted inline: duplicate HTTP client vars across packages (future tuning gap) and mutable package-level var (test-pollution risk).

🔎 Code quality review by PR Code Quality Reviewer · 76.8 AIC · ⌖ 7.45 AIC · ⊞ 1.6K
Comment /review to run again

if host == "" || host == "github.com" {
remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s@%s", owner, repo, ref)
return resolveRefToSHAViaPublicAPI(owner, repo, ref)
return resolveRefToSHAViaPublicAPI(context.Background(), owner, repo, ref)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Incomplete context threading: context.Background() silently drops all caller cancellation signals at every public-API fallback site.

💡 Details and suggested fix

Five call sites in this file (resolveRefToSHA, downloadFileFromGitHubWithDepth, listWorkflowFilesForHost, listDirAllFilesForHost, listDirSubdirsForHost) pass context.Background() because the outer functions have no ctx parameter. This means:

  • Ctrl-C / OS signal → context cancelled at the CLI layer → the HTTP request in publicAPIClient.Do(req) continues for up to 30 s regardless.
  • Any caller with a short deadline (e.g., a test with t.Context()) has that deadline silently discarded.
  • The Timeout field on publicAPIClient is the only real protection; the new ctx parameters are scaffolding with no current effect on cancellation.

Complete the threading by adding ctx to the outer functions:

// was: func resolveRefToSHA(owner, repo, ref, host string) (string, error)
func resolveRefToSHA(ctx context.Context, owner, repo, ref, host string) (string, error) {
    // ...
    return resolveRefToSHAViaPublicAPI(ctx, owner, repo, ref) // real ctx, not context.Background()
}

Same pattern for the other four outer wrappers. Without this, the PR description overstates the change — context is not actually threaded, only a 30 s client-level timeout is added.

// updatePublicAPIClient is a shared HTTP client used for unauthenticated GitHub
// API fallback calls in the update workflow path. It carries a timeout to
// prevent indefinite hangs on slow or unresponsive hosts.
var updatePublicAPIClient = &http.Client{Timeout: constants.DefaultHTTPClientTimeout}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Duplicate HTTP client declarations across two packages create a silent maintenance trap.

💡 Details

updatePublicAPIClient (here) and publicAPIClient (pkg/parser/remote_fetch.go:36) are structurally identical:

&http.Client{Timeout: constants.DefaultHTTPClientTimeout}

When the unauthenticated API path eventually needs tuning — custom Transport for proxy support, different timeout per environment, retry middleware — the change will be applied to one and silently missed in the other. A shared constructor (or a single exported client) in a common internal/httpclient or constants package would close this gap:

// e.g., in pkg/constants or pkg/internal/httpclient
func NewPublicAPIClient() *http.Client {
    return &http.Client{Timeout: DefaultHTTPClientTimeout}
}

Both call sites then call httpclient.NewPublicAPIClient(), giving a single place to evolve the configuration.

// publicAPIClient is a shared HTTP client used for unauthenticated GitHub API
// fallback calls. It carries a timeout to prevent indefinite hangs on slow or
// unresponsive hosts.
var publicAPIClient = &http.Client{Timeout: constants.DefaultHTTPClientTimeout}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mutable package-level var for the HTTP client enables test-state pollution.

💡 Details

publicAPIClient is a plain var, which means any in-package test can overwrite it:

// In a test file
func TestSomething(t *testing.T) {
    publicAPIClient = &http.Client{Transport: mockTransport}
    // if this test panics before cleanup, next test picks up mockTransport
}

If a test fails before restoring the original value (or uses t.Parallel()), subsequent tests silently use the wrong transport. The standard Go mitigation is to save and restore via t.Cleanup, but that requires discipline across every future test that needs to inject a mock. A safer pattern is a function-level or injected client:

// Unexported sentinel; set once at init, replaced in tests via a setter
var publicAPIClient = newPublicAPIClient()

func newPublicAPIClient() *http.Client {
    return &http.Client{Timeout: constants.DefaultHTTPClientTimeout}
}

Tests swap via the constructor rather than mutating the global, which makes the scope of the change explicit and t.Cleanup-friendly.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnosing-bugs, /tdd, and /codebase-design — requesting changes primarily for the missing regression test.

📋 Key Themes & Highlights

Key Themes

  • Missing regression test (blocking): The goroutine-hang fix has no test. If a future change reverts to http.DefaultClient, the bug silently returns. A single httptest.NewServer-based test would lock this in permanently.
  • context.Background() at call sites: All 5 new call sites pass context.Background() because parent functions lack a ctx parameter. Correct for the current scope, but worth tracking for future context-propagation work.
  • Duplicate HTTP client config: publicAPIClient and updatePublicAPIClient are identical — low risk now, but could diverge silently.

Positive Highlights

  • ✅ Surgical fix: minimal diff, clear intent, all relevant fallback paths covered consistently
  • http.NewRequestWithContext used correctly in every new request
  • ✅ Comments on the new package-level vars clearly explain the timeout rationale
  • ✅ Call sites that lack ambient context correctly pass context.Background(), matching the existing pattern in the file
  • ✅ Centralising the client in a package-level var (rather than per-call) is the right call for a shared timeout

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 54.9 AIC · ⌖ 9.13 AIC · ⊞ 6.5K
Comment /matt to run again

// publicAPIClient is a shared HTTP client used for unauthenticated GitHub API
// fallback calls. It carries a timeout to prevent indefinite hangs on slow or
// unresponsive hosts.
var publicAPIClient = &http.Client{Timeout: constants.DefaultHTTPClientTimeout}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs + /tdd] No regression test guards this fix — if a future change silently reverts to http.DefaultClient, the hang will come back undetected.

💡 Suggested test skeleton

A test using httptest.NewServer with a slow handler and a short-timeout client would confirm the fix:

func TestPublicAPIClientTimeout(t *testing.T) {
    slow := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(2 * time.Second)
    }))
    defer slow.Close()

    orig := publicAPIClient
    publicAPIClient = &http.Client{Timeout: 100 * time.Millisecond}
    t.Cleanup(func() { publicAPIClient = orig })

    _, err := fetchPublicGitHubContentsAPI(context.Background(), "owner", "repo", "path", "ref")
    if err == nil {
        t.Fatal("expected timeout error, got nil")
    }
}

This acts as a living specification: any regression that swaps back to http.DefaultClient fails immediately.

@copilot please address this.

if host == "" || host == "github.com" {
remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s@%s", owner, repo, ref)
return resolveRefToSHAViaPublicAPI(owner, repo, ref)
return resolveRefToSHAViaPublicAPI(context.Background(), owner, repo, ref)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs] All five new context.Background() call sites forfeit context cancellation — if a caller later gains a deadline (e.g. a per-request context from a CLI command), the HTTP call will not respect it; the 30 s timeout is the only guard.

💡 Why this matters and what to consider

The parent functions (resolveRefToSHA, downloadFileFromGitHubWithDepth, listWorkflowFilesForHost, etc.) currently have no context.Context parameter, so context.Background() is the only viable option right now. This PR is correct as-is, but leaves a TODO in the door:

  • Short term: add a code comment at each call site, e.g. // TODO(context): pass caller ctx once parent accepts one
  • Longer term: thread ctx through the parent functions so cancellation signals from the CLI propagate end-to-end

Without this, a user cancelling a long-running gh aw compile will cancel everything except the last-resort public API fallbacks, which must wait for the full 30 s.

@copilot please address this.

// updatePublicAPIClient is a shared HTTP client used for unauthenticated GitHub
// API fallback calls in the update workflow path. It carries a timeout to
// prevent indefinite hangs on slow or unresponsive hosts.
var updatePublicAPIClient = &http.Client{Timeout: constants.DefaultHTTPClientTimeout}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/codebase-design] updatePublicAPIClient is a second package-level HTTP client with the same configuration as publicAPIClient in pkg/parser/remote_fetch.go. If the timeout value ever needs to change (e.g. for a shorter default), both declarations must be updated independently.

💡 Consider consolidating

Both clients are: &http.Client{Timeout: constants.DefaultHTTPClientTimeout}. The duplication is currently low-risk because constants.DefaultHTTPClientTimeout is the single source of truth for the value. However, the two clients could diverge in future (e.g. one gets headers, retries, or a transport).

A shared constructor or a single pkg/httputil package-level var would make the intent clearer and reduce the divergence risk. For now, a comment linking the two declarations would also help:

// See also publicAPIClient in pkg/parser/remote_fetch.go — same config.
var updatePublicAPIClient = &http.Client{Timeout: constants.DefaultHTTPClientTimeout}

@copilot please address this.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category bug
Risk 🟡 Medium
Score 40 / 100
Action defer

Score breakdown: Impact 25/50 · Urgency 12/30 · Quality 3/20

Rationale: Fixes goroutine hang in unauthenticated GitHub API paths by adding 30s timeout. Currently blocked by changes_requested from CI. Resolve review feedback to unblock.

Generated by 🔧 PR Triage Agent · 83.9 AIC · ⌖ 17.1 AIC · ⊞ 1.6K ·

Copilot AI commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Ran the pr-finisher pass.

  • ✅ Reviews — no new actionable items from the explicit @copilot comment in this task.
  • ✅ Checks (local) — make fmt, make lint, make test-unit, make test, and make recompile completed successfully.
  • ❓ Checks (CI) — stale after agent push model; latest snapshot shows prior canceled runs and one in-progress run, with no new actionable failed logs in this pass.
  • ❓ Mergeable — GitHub GraphQL gh pr view is returning 403 in this environment, so mergeability could not be confirmed from CLI.

Actions taken: executed triage + local validation and confirmed no additional code changes were required for this requested pass.
Hand-off: CI must be re-triggered by a maintainer (close/reopen PR, workflow_dispatch, or maintainer push) before merge.

@pelikhan pelikhan merged commit 78d1644 into main Jun 30, 2026
85 of 96 checks passed
@pelikhan pelikhan deleted the copilot/deep-report-harden-github-api-fallbacks branch June 30, 2026 19:41
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[deep-report] Harden 3 unauthenticated GitHub API fallbacks with context + timeout

3 participants