From cb6d447e9890c1944443ec096438123cc20ed53d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:18:37 +0000 Subject: [PATCH 1/3] Initial plan From ed06cff72219ea45cfbf1eb4cec876acaa042b4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:34:39 +0000 Subject: [PATCH 2/3] fix: harden unauthenticated GitHub API fallbacks with context and timeout - 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> --- pkg/cli/update_workflows.go | 2 +- pkg/parser/remote_fetch.go | 40 +++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/pkg/cli/update_workflows.go b/pkg/cli/update_workflows.go index 5758b005dcf..7d488299061 100644 --- a/pkg/cli/update_workflows.go +++ b/pkg/cli/update_workflows.go @@ -364,7 +364,7 @@ func fetchPublicGitHubAPI(ctx context.Context, endpoint string) ([]byte, error) } req.Header.Set("Accept", "application/vnd.github+json") - resp, err := http.DefaultClient.Do(req) + resp, err := (&http.Client{Timeout: constants.DefaultHTTPClientTimeout}).Do(req) if err != nil { return nil, err } diff --git a/pkg/parser/remote_fetch.go b/pkg/parser/remote_fetch.go index 7c183de4404..34484951ea4 100644 --- a/pkg/parser/remote_fetch.go +++ b/pkg/parser/remote_fetch.go @@ -527,7 +527,7 @@ func resolveRefToSHA(owner, repo, ref, host string) (string, error) { if gitErr != nil { 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) } return "", fmt.Errorf("failed to resolve ref via GitHub API (auth error) and git ls-remote: API error: %w, Git error: %w", err, gitErr) } @@ -559,17 +559,18 @@ func buildCommitLookupAPIPath(owner, repo, ref string) string { // resolveRefToSHAViaPublicAPI resolves a git ref to its commit SHA using an // unauthenticated call to the public GitHub API. Used as a last-resort fallback // when both authenticated API and git ls-remote fail. -func resolveRefToSHAViaPublicAPI(owner, repo, ref string) (string, error) { +func resolveRefToSHAViaPublicAPI(ctx context.Context, owner, repo, ref string) (string, error) { remoteLog.Printf("Attempting unauthenticated public API ref resolution for %s/%s@%s", owner, repo, ref) apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/commits/%s", owner, repo, url.PathEscape(ref)) - req, err := http.NewRequest(http.MethodGet, apiURL, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) if err != nil { return "", err } req.Header.Set("Accept", "application/vnd.github+json") - resp, err := http.DefaultClient.Do(req) + publicAPIClient := &http.Client{Timeout: constants.DefaultHTTPClientTimeout} + resp, err := publicAPIClient.Do(req) if err != nil { return "", err } @@ -922,7 +923,7 @@ func downloadFileFromGitHubWithDepth(owner, repo, path, ref string, symlinkDepth if gitErr != nil { if host == "" || host == "github.com" { remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s/%s@%s", owner, repo, path, ref) - return downloadFileViaPublicAPI(owner, repo, path, ref) + return downloadFileViaPublicAPI(context.Background(), owner, repo, path, ref) } return nil, fmt.Errorf("failed to fetch file content via GitHub API (auth error) and git fallback: API error: %w, Git error: %w", err, gitErr) } @@ -979,9 +980,9 @@ func fetchRemoteFileContent(client *api.RESTClient, owner, repo, path, ref strin // downloadFileViaPublicAPI downloads a file from a public GitHub repository // using an unauthenticated API call. Used as a last-resort fallback when both // authenticated API and git clone fail (e.g. enterprise SAML tokens). -func downloadFileViaPublicAPI(owner, repo, path, ref string) ([]byte, error) { +func downloadFileViaPublicAPI(ctx context.Context, owner, repo, path, ref string) ([]byte, error) { remoteLog.Printf("Attempting unauthenticated public API download for %s/%s/%s@%s", owner, repo, path, ref) - body, err := fetchPublicGitHubContentsAPI(owner, repo, path, ref) + body, err := fetchPublicGitHubContentsAPI(ctx, owner, repo, path, ref) if err != nil { return nil, fmt.Errorf("unauthenticated public API also failed for %s/%s/%s@%s: %w", owner, repo, path, ref, err) } @@ -1062,7 +1063,7 @@ func listWorkflowFilesForHost(owner, repo, ref, workflowPath, host string) ([]st if gitErr != nil { if host == "" || host == "github.com" { remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s@%s", owner, repo, ref) - return listWorkflowFilesViaPublicAPI(owner, repo, ref, workflowPath) + return listWorkflowFilesViaPublicAPI(context.Background(), owner, repo, ref, workflowPath) } return nil, fmt.Errorf("failed to list workflow files via GitHub API (auth error) and git fallback: API error: %w, Git error: %w", err, gitErr) } @@ -1116,7 +1117,7 @@ func listDirAllFilesForHost(owner, repo, ref, dirPath, host string) ([]string, e if gitErr != nil { if host == "" || host == "github.com" { remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s@%s", owner, repo, ref) - return listDirAllFilesViaPublicAPI(owner, repo, ref, dirPath) + return listDirAllFilesViaPublicAPI(context.Background(), owner, repo, ref, dirPath) } return nil, fmt.Errorf("failed to list dir files via API (auth error) and git fallback: API error: %w, Git error: %w", err, gitErr) } @@ -1172,9 +1173,9 @@ func listDirAllFilesViaGitForHost(owner, repo, ref, dirPath, host string) ([]str // listDirAllFilesViaPublicAPI lists files in a directory using an unauthenticated // call to the public GitHub API. Used as a last-resort fallback when both // authenticated API and git clone fail. -func listDirAllFilesViaPublicAPI(owner, repo, ref, dirPath string) ([]string, error) { +func listDirAllFilesViaPublicAPI(ctx context.Context, owner, repo, ref, dirPath string) ([]string, error) { remoteLog.Printf("Attempting unauthenticated public API for listing dir files: %s/%s@%s (path: %s)", owner, repo, ref, dirPath) - body, err := fetchPublicGitHubContentsAPI(owner, repo, dirPath, ref) + body, err := fetchPublicGitHubContentsAPI(ctx, owner, repo, dirPath, ref) if err != nil { return nil, fmt.Errorf("unauthenticated public API also failed for %s/%s@%s (path: %s): %w", owner, repo, ref, dirPath, err) } @@ -1311,7 +1312,7 @@ func listDirAllFilesRecursivelyViaGitForHost(owner, repo, ref, dirPath, host str // cannot access cross-organization public repositories and git clone also // fails. Unauthenticated requests are subject to a lower rate limit // (60 req/hour) but are sufficient for the handful of calls during update. -func fetchPublicGitHubContentsAPI(owner, repo, path, ref string) ([]byte, error) { +func fetchPublicGitHubContentsAPI(ctx context.Context, owner, repo, path, ref string) ([]byte, error) { // Encode each path segment independently so that '/' separators are // preserved — url.PathEscape would turn them into '%2F', breaking nested // paths like '.github/workflows/shared/foo.md'. @@ -1322,13 +1323,14 @@ func fetchPublicGitHubContentsAPI(owner, repo, path, ref string) ([]byte, error) } endpoint := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, strings.Join(encodedSegments, "/"), url.QueryEscape(ref)) - req, err := http.NewRequest(http.MethodGet, endpoint, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/vnd.github+json") - resp, err := http.DefaultClient.Do(req) + publicAPIClient := &http.Client{Timeout: constants.DefaultHTTPClientTimeout} + resp, err := publicAPIClient.Do(req) if err != nil { return nil, err } @@ -1375,7 +1377,7 @@ func listDirSubdirsForHost(owner, repo, ref, dirPath, host string) ([]string, er if gitErr != nil { if host == "" || host == "github.com" { remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s@%s", owner, repo, ref) - return listDirSubdirsViaPublicAPI(owner, repo, ref, dirPath) + return listDirSubdirsViaPublicAPI(context.Background(), owner, repo, ref, dirPath) } return nil, fmt.Errorf("failed to list subdirs via API (auth error) and git fallback: API error: %w, Git error: %w", err, gitErr) } @@ -1431,9 +1433,9 @@ func listDirSubdirsViaGitForHost(owner, repo, ref, dirPath, host string) ([]stri // listDirSubdirsViaPublicAPI lists subdirectories using an unauthenticated call // to the public GitHub API. Used as a last-resort fallback when both // authenticated API and git clone fail (e.g. enterprise SAML tokens). -func listDirSubdirsViaPublicAPI(owner, repo, ref, dirPath string) ([]string, error) { +func listDirSubdirsViaPublicAPI(ctx context.Context, owner, repo, ref, dirPath string) ([]string, error) { remoteLog.Printf("Attempting unauthenticated public API for listing subdirs: %s/%s@%s (path: %s)", owner, repo, ref, dirPath) - body, err := fetchPublicGitHubContentsAPI(owner, repo, dirPath, ref) + body, err := fetchPublicGitHubContentsAPI(ctx, owner, repo, dirPath, ref) if err != nil { return nil, fmt.Errorf("unauthenticated public API also failed for %s/%s@%s (path: %s): %w", owner, repo, ref, dirPath, err) } @@ -1515,9 +1517,9 @@ func listWorkflowFilesViaGitForHost(owner, repo, ref, workflowPath, host string) // listWorkflowFilesViaPublicAPI lists workflow .md files using an unauthenticated // call to the public GitHub API. Used as a last-resort fallback when both // authenticated API and git clone fail. -func listWorkflowFilesViaPublicAPI(owner, repo, ref, workflowPath string) ([]string, error) { +func listWorkflowFilesViaPublicAPI(ctx context.Context, owner, repo, ref, workflowPath string) ([]string, error) { remoteLog.Printf("Attempting unauthenticated public API for listing workflow files: %s/%s@%s (path: %s)", owner, repo, ref, workflowPath) - body, err := fetchPublicGitHubContentsAPI(owner, repo, workflowPath, ref) + body, err := fetchPublicGitHubContentsAPI(ctx, owner, repo, workflowPath, ref) if err != nil { return nil, fmt.Errorf("unauthenticated public API also failed for %s/%s@%s (path: %s): %w", owner, repo, ref, workflowPath, err) } From 5483bc3e3b47fb382bf35c0e130024fdd1978227 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:37:48 +0000 Subject: [PATCH 3/3] refactor: use package-level HTTP clients for unauthenticated API fallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- pkg/cli/update_workflows.go | 7 ++++++- pkg/parser/remote_fetch.go | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/cli/update_workflows.go b/pkg/cli/update_workflows.go index 7d488299061..7be166979c3 100644 --- a/pkg/cli/update_workflows.go +++ b/pkg/cli/update_workflows.go @@ -26,6 +26,11 @@ import ( var defaultBranchCache sync.Map var branchCommitCache sync.Map +// 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} + // repoBranchKey is a composite cache key for branch commit SHA lookups. type repoBranchKey struct { repo string @@ -364,7 +369,7 @@ func fetchPublicGitHubAPI(ctx context.Context, endpoint string) ([]byte, error) } req.Header.Set("Accept", "application/vnd.github+json") - resp, err := (&http.Client{Timeout: constants.DefaultHTTPClientTimeout}).Do(req) + resp, err := updatePublicAPIClient.Do(req) if err != nil { return nil, err } diff --git a/pkg/parser/remote_fetch.go b/pkg/parser/remote_fetch.go index 34484951ea4..253d2f386f5 100644 --- a/pkg/parser/remote_fetch.go +++ b/pkg/parser/remote_fetch.go @@ -30,6 +30,11 @@ import ( var remoteLog = logger.New("parser:remote_fetch") +// 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} + // gitListCloneCache is a process-lifetime cache of shallow clones used by // git-based directory listing fallbacks to avoid repeated clone operations for // the same repository/ref tuple. Entries are not explicitly cleaned up because @@ -569,7 +574,6 @@ func resolveRefToSHAViaPublicAPI(ctx context.Context, owner, repo, ref string) ( } req.Header.Set("Accept", "application/vnd.github+json") - publicAPIClient := &http.Client{Timeout: constants.DefaultHTTPClientTimeout} resp, err := publicAPIClient.Do(req) if err != nil { return "", err @@ -1329,7 +1333,6 @@ func fetchPublicGitHubContentsAPI(ctx context.Context, owner, repo, path, ref st } req.Header.Set("Accept", "application/vnd.github+json") - publicAPIClient := &http.Client{Timeout: constants.DefaultHTTPClientTimeout} resp, err := publicAPIClient.Do(req) if err != nil { return nil, err