fix: retry GitHub REST API calls when HTML is returned instead of JSON#22800
fix: retry GitHub REST API calls when HTML is returned instead of JSON#22800
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/e99d386d-c5e4-4c8d-a8ed-98bba745705b
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/e99d386d-c5e4-4c8d-a8ed-98bba745705b
There was a problem hiding this comment.
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
executeUpdatecalls withwithRetryto 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/prependinupdateBody), if the first request actually succeeds but the client errors (timeouts, connection reset, etc.). Since this factory now retries on broad transient patterns, consider narrowingshouldRetryfor 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.
withRetrymultipliesdelayafter the first failure before sleeping, so with{ initialDelayMs: 2000 }and defaultbackoffMultiplier: 2, the first retry will wait ~4000ms. If you want the first retry to be ~2000ms, consider settinginitialDelayMsto 1000 (keeping the default multiplier) or explicitly settingbackoffMultiplier: 1for 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.
| // 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}`); |
There was a problem hiding this comment.
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
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.cjs—isTransientErrornow 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— wrapsexecuteUpdatewithwithRetry(1 retry, 2s delay) so all update handlers (update_pull_request,update_issue,update_discussion) automatically retry on transient errors including HTML responses.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/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/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/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 '**/*.cjs' '*git(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v3/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/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)/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)/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/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)/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)/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/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha ./../pkg/workflo-s(http block)/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)/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/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/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/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' --git(http block)https://api.github.com/repos/github/gh-aw-actions/git/ref/tags/v1.0.0/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/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/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/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/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/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/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/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/usr/bin/gh gh run download 5 --dir test-logs/run-5(http block)https://api.github.com/repos/github/gh-aw/actions/workflows/usr/bin/gh gh workflow list --json name,state,path ../pkg/workflow/js/**/*.json' --ignore-path ../../../.prettierignore --global 64/bin/bash http.https://git/opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet(http block)/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)/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/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/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha te '../../../**/*.json' '!../../../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/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.2.3 --jq .object.sha '**/*.ts' '**/*.json' --ignore-path ../../../.pr**/*.json(http block)https://api.github.com/repos/github/gh-aw/git/ref/tags/v2.0.0/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)/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha get(http block)/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/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/usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha te '../../../**/-c=4 .cfg 64/pkg/tool/linu-importcfg(http block)https://api.github.com/repos/nonexistent/repo/actions/runs/12345/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/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)/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/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/usr/bin/gh gh api /repos/test-owner/test-repo/actions/secrets --jq .secrets[].name(http block)invalid.example.invalid/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.