Skip to content

fix: retry GitHub REST API calls when HTML is returned instead of JSON#22800

Merged
pelikhan merged 3 commits intomainfrom
copilot/handle-api-crash-retry
Mar 25, 2026
Merged

fix: retry GitHub REST API calls when HTML is returned instead of JSON#22800
pelikhan merged 3 commits intomainfrom
copilot/handle-api-crash-retry

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 25, 2026

GitHub's REST API occasionally crashes and returns an HTML 500 "Unicorn!" page instead of JSON. Previously this caused update_pull_request (and other update handlers) to fail permanently with no retry.

Changes

  • error_recovery.cjsisTransientError now detects HTML responses by checking if the error message starts with <!DOCTYPE html. GitHub's Unicorn page is a transient 500, not a permanent failure.

  • update_handler_factory.cjs — wraps executeUpdate with withRetry (1 retry, 2s delay) so all update handlers (update_pull_request, update_issue, update_discussion) automatically retry on transient errors including HTML responses.

// Before: no retry
const updatedItem = await executeUpdate(githubClient, effectiveContext, itemNumber, updateData);

// After: retries once on transient errors (HTML responses, network errors, rate limits)
const updatedItem = await withRetry(
  () => executeUpdate(githubClient, effectiveContext, itemNumber, updateData),
  { maxRetries: 1, initialDelayMs: 2000, shouldRetry: isTransientError },
  `update ${itemTypeName} #${itemNumber}`,
);

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw --write 64/pkg/tool/linu--show-toplevel git rev-�� --show-toplevel 64/pkg/tool/linux_amd64/vet /usr/bin/git format:pkg-json /update_pull_reqrev-parse .cfg git (http block)
  • https://api.github.com/orgs/test-owner/actions/secrets
    • Triggering command: /usr/bin/gh gh api /orgs/test-owner/actions/secrets --jq .secrets[].name (http block)
  • https://api.github.com/repos/actions/ai-inference/git/ref/tags/v1
    • Triggering command: /usr/bin/gh gh api /repos/actions/ai-inference/git/ref/tags/v1 --jq .object.sha --show-toplevel x_amd64/vet /usr/bin/git celain --ignore-git origin x_amd64/vet git rev-�� --show-toplevel x_amd64/vet /opt/hostedtoolcache/node/24.14.0/x64/bin/node te &#39;**/*.cjs&#39; &#39;*git (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v3
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha -bool -buildtags ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -errorsas -ifaceassert -nilfunc ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v5
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha 7Kd4r1pAq .cfg 64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha steps.test.outputs.result x_amd64/vet /usr/bin/git date_pull_requesgit /home/REDACTED/worrev-parse 64/pkg/tool/linu--show-toplevel git rev-�� --show-toplevel 64/pkg/tool/linux_amd64/vet /usr/bin/git --exclude-standagit on 64/pkg/tool/linu--show-toplevel git (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --show-toplevel ache/go/1.25.0/x64/pkg/tool/linu/home/REDACTED/work/gh-aw/gh-aw/scripts/lint_error_messages_test.grev-parse /usr/bin/git se 8235622/b212/vetrev-parse ache/go/1.25.0/x--show-toplevel git rev-�� --show-toplevel ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet 0/x64/bin/node k/gh-aw/gh-aw/ingit actions/setup/jsrev-parse ache/go/1.25.0/x--show-toplevel git (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v6
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha /tmp/go-build1208235622/b445/_pkg_.a -trimpath /usr/bin/git -p github.com/githurev-parse -lang=go1.25 git rev-�� nt/action/git/ref/tags/v999.999.999 -goversion /usr/bin/git -c=4 -nolocalimports -importcfg git (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha --show-toplevel git /usr/bin/git t.test.cjs credential.usernrev-parse x_amd64/vet git add .github/workflows/test.md x_amd64/vet /usr/bin/git rd gpg.program x_amd64/vet git (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha --show-toplevel 64/pkg/tool/linux_amd64/vet /usr/bin/git se 8235622/b057/vetrev-parse .cfg git rev-�� --show-toplevel ache/go/1.25.0/x64/pkg/tool/linutest@example.com /usr/bin/git se 8235622/b204/vetrev-parse .cfg git (http block)
  • https://api.github.com/repos/actions/github-script/git/ref/tags/v8
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha ./../pkg/workflo-s (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha t.cjs --local tions/node_modul-nilfunc credential.helpe/opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha t.cjs tions/setup/js/a-ifaceassert tions/setup/js/a-nilfunc tions/setup/js/a/opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet tions/setup/js/a-atomic tions/setup/js/a-bool tions/setup/js/a-buildtags tion�� tory.test.cjs tions/setup/js/a-ifaceassert k/_temp/uv-pytho-nilfunc commit.gpgsign (http block)
  • https://api.github.com/repos/actions/setup-go/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha -bool -buildtags /usr/bin/git -errorsas -ifaceassert -nilfunc git rev-�� --git-dir -tests /usr/bin/git nsientError k/gh-aw/gh-aw/acrev-parse x_amd64/vet git (http block)
  • https://api.github.com/repos/actions/setup-node/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha /tmp/TestHashConsistency_GoAndJavaScript3217763497/001/test-fronnonexistent/repo git /usr/bin/git t.test.cjs credential.usernrev-parse x_amd64/compile git -C /tmp/gh-aw-test-runs/20260325-005841-13441/test-965177034 status /usr/bin/git .github/workflowgit k/gh-aw/gh-aw/acrev-parse x_amd64/vet git (http block)
  • https://api.github.com/repos/actions/upload-artifact/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v4 --jq .object.sha -unreachable=false /tmp/go-build1208235622/b055/vet.cfg 8235622/b326/vet.cfg js/**/*.json&#39; --git (http block)
  • https://api.github.com/repos/github/gh-aw-actions/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw-actions/git/ref/tags/v1.0.0 --jq .object.sha runs/20260325-005841-13441/test-3902930396/.github/workflows /tmp/go-build1208235622/b119/vet.cfg /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet l (http block)
  • https://api.github.com/repos/github/gh-aw-actions/git/ref/tags/v1.2.3
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw-actions/git/ref/tags/v1.2.3 --jq .object.sha runs/20260325-005841-13441/test-3902930396/.github/workflows -buildtags 8235622/b382/vet.cfg l -ifaceassert -nilfunc /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -uns�� k/gh-aw/gh-aw/.github/workflows /tmp/go-build1208235622/b203/vet.cfg 8235622/b441/styles.test (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/1/artifacts
    • Triggering command: /usr/bin/gh gh run download 1 --dir test-logs/run-1 (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/12345/artifacts
    • Triggering command: /usr/bin/gh gh run download 12345 --dir test-logs/run-12345 (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/12346/artifacts
    • Triggering command: /usr/bin/gh gh run download 12346 --dir test-logs/run-12346 (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/2/artifacts
    • Triggering command: /usr/bin/gh gh run download 2 --dir test-logs/run-2 (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/3/artifacts
    • Triggering command: /usr/bin/gh gh run download 3 --dir test-logs/run-3 (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/4/artifacts
    • Triggering command: /usr/bin/gh gh run download 4 --dir test-logs/run-4 (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/5/artifacts
    • Triggering command: /usr/bin/gh gh run download 5 --dir test-logs/run-5 (http block)
  • https://api.github.com/repos/github/gh-aw/actions/workflows
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path ../pkg/workflow/js/**/*.json&#39; --ignore-path ../../../.prettierignore --global 64/bin/bash http.https://git/opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 100 (http block)
    • Triggering command: /usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 6 r /home/REDACTED/wor.github/workflows/test.md x_amd64/link k/gh�� copilot/handle-api-crash-retry on 64/pkg/tool/linux_amd64/vet /../../.prettiergit erignore /home/REDACTED/wor--show-toplevel rk/cnjHJRsC75-1TI1_m0Vn/uTsMpJMIC8zTipfMwGSL (http block)
  • https://api.github.com/repos/github/gh-aw/contents/.github%2Fworkflows%2Faudit-workflows.md
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node --experimental-import-meta-resolve --require /home/REDACTED/.npm/_npx/69c381f8ad94b576/node_modules/vitest/suppress-warnings.cjs --conditions node --conditions development /home/REDACTED/.npm/_npx/69c381f8ad94b576/node_modules/vitest/dist/workers/forks.js s-branch..HEAD git git bran�� -M main t /tmp/push-signedgit (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha te &#39;../../../**/*.json&#39; &#39;!../../../pkg/workflow/-errorsas .cfg 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.2.3
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.2.3 --jq .object.sha &#39;**/*.ts&#39; &#39;**/*.json&#39; --ignore-path ../../../.pr**/*.json (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v2.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha tions/setup/js/m-errorsas (http block)
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha get (http block)
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha get ules/vitest/supp-ifaceassert ache/node/24.14.-nilfunc committer.email (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v3.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v3.0.0 --jq .object.sha get (http block)
  • https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999
    • Triggering command: /usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha te &#39;../../../**/-c=4 .cfg 64/pkg/tool/linu-importcfg (http block)
  • https://api.github.com/repos/nonexistent/repo/actions/runs/12345
    • Triggering command: /usr/bin/gh gh run view 12345 --repo nonexistent/repo --json status,conclusion rkflow/js/**/*.jgit (http block)
  • https://api.github.com/repos/owner/repo/actions/workflows
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path --repo owner/repo ndor/bin/bash http.https://git/opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path --repo owner/repo e/git-remote-https http.https://git/opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/owner/repo/contents/file.md
    • Triggering command: /tmp/go-build1208235622/b402/cli.test /tmp/go-build1208235622/b402/cli.test -test.testlogfile=/tmp/go-build1208235622/b402/testlog.txt -test.paniconexit0 -test.v=true -test.parallel=4 -test.timeout=10m0s -test.run=^Test -test.short=true (http block)
  • https://api.github.com/repos/test-owner/test-repo/actions/secrets
    • Triggering command: /usr/bin/gh gh api /repos/test-owner/test-repo/actions/secrets --jq .secrets[].name (http block)
  • invalid.example.invalid
    • Triggering command: /usr/lib/git-core/git-remote-https /usr/lib/git-core/git-remote-https origin https://invalid.example.invalid/nonexistent-repo.git git show�� --verify --quiet it -b ch de/node/bin/git git init�� bin/git lure modules/@npmcli/run-script/lib/node-gyp-bin/git main git m/_npx/69c381f8aagent-change.txt git (dns block)

If you need me to access, download, or install something from one of these locations, you can either:


📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.

Copilot AI changed the title [WIP] Add retry logic for GitHub REST API crash fix: retry GitHub REST API calls when HTML is returned instead of JSON Mar 25, 2026
Copilot AI requested a review from pelikhan March 25, 2026 01:10
@pelikhan pelikhan marked this pull request as ready for review March 25, 2026 01:23
Copilot AI review requested due to automatic review settings March 25, 2026 01:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds resilience to GitHub REST update handlers by detecting GitHub’s occasional HTML “Unicorn” error pages as transient failures and retrying updates once instead of failing permanently.

Changes:

  • Extend transient-error detection to treat HTML <!doctype html... responses as retryable.
  • Wrap all update-handler executeUpdate calls with withRetry to retry once on transient failures.
  • Adjust an existing unit test expectation to account for enhanced error text after retries.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
actions/setup/js/error_recovery.cjs Treats HTML-doctype error messages as transient and normalizes casing/whitespace handling.
actions/setup/js/error_recovery.test.cjs Adds tests ensuring HTML “Unicorn” responses are classified as transient.
actions/setup/js/update_handler_factory.cjs Wraps update execution in withRetry to add a single automatic retry for transient errors.
actions/setup/js/update_handler_factory.test.cjs Loosens assertion on error text to allow enhanced error messages from retry wrapper.
Comments suppressed due to low confidence (2)

actions/setup/js/update_handler_factory.cjs:260

  • Retrying update operations can duplicate side effects for non-idempotent body operations (notably append/prepend in updateBody), if the first request actually succeeds but the client errors (timeouts, connection reset, etc.). Since this factory now retries on broad transient patterns, consider narrowing shouldRetry for update handlers (or making append/prepend updates idempotent, e.g. via replace-island markers) to avoid double-appending content.
      // Retry on transient errors (e.g. GitHub API returning HTML instead of JSON on 500 crashes).
      try {
        const updatedItem = await withRetry(() => executeUpdate(githubClient, effectiveContext, itemNumber, updateData), { maxRetries: 1, initialDelayMs: 2000, shouldRetry: isTransientError }, `update ${itemTypeName} #${itemNumber}`);
        core.info(`Successfully updated ${itemTypeName} #${itemNumber}: ${updatedItem.html_url || updatedItem.url}`);

actions/setup/js/update_handler_factory.cjs:259

  • The retry delay here likely isn't the intended 2s. withRetry multiplies delay after the first failure before sleeping, so with { initialDelayMs: 2000 } and default backoffMultiplier: 2, the first retry will wait ~4000ms. If you want the first retry to be ~2000ms, consider setting initialDelayMs to 1000 (keeping the default multiplier) or explicitly setting backoffMultiplier: 1 for this call site.
        const updatedItem = await withRetry(() => executeUpdate(githubClient, effectiveContext, itemNumber, updateData), { maxRetries: 1, initialDelayMs: 2000, shouldRetry: isTransientError }, `update ${itemTypeName} #${itemNumber}`);

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

Comment on lines +257 to 260
// Retry on transient errors (e.g. GitHub API returning HTML instead of JSON on 500 crashes).
try {
const updatedItem = await executeUpdate(githubClient, effectiveContext, itemNumber, updateData);
const updatedItem = await withRetry(() => executeUpdate(githubClient, effectiveContext, itemNumber, updateData), { maxRetries: 1, initialDelayMs: 2000, shouldRetry: isTransientError }, `update ${itemTypeName} #${itemNumber}`);
core.info(`Successfully updated ${itemTypeName} #${itemNumber}: ${updatedItem.html_url || updatedItem.url}`);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

This change introduces new behavior (retrying executeUpdate on transient errors), but update_handler_factory.test.cjs doesn't currently assert that retries happen (e.g., executeUpdate called twice when the first attempt throws a transient error). Adding a test for the retry path would help prevent regressions and confirm the correct retry config (attempt count + delay/backoff).

This issue also appears in the following locations of the same file:

  • line 257
  • line 259

Copilot uses AI. Check for mistakes.
@pelikhan pelikhan merged commit 45a339e into main Mar 25, 2026
103 checks passed
@pelikhan pelikhan deleted the copilot/handle-api-crash-retry branch March 25, 2026 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants