{"record":{"id":"12a6379299582e53","repo":"paperclipai/paperclip","slug":"github-webhook-recovery-http","errorCode":"github_webhook_recovery_http","errorMessage":"github_webhook_recovery_http","messagePattern":"github_webhook_recovery_http","errorType":"error_code","errorClass":"GitHubWebhookRecoveryError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-github-webhook-config.ts","lineNumber":364,"sourceCode":"              authorization: `Bearer ${input.token}`,\n              \"x-github-api-version\": \"2022-11-28\",\n              ...(input.body ? { \"content-type\": \"application/json\" } : {}),\n            },\n            ...(input.body ? { body: input.body } : {}),\n          },\n        );\n        if (controller.signal.aborted) {\n          await response.body?.cancel().catch(() => undefined);\n          throw new GitHubWebhookRecoveryError(\n            \"github_webhook_recovery_transport\",\n            null,\n            null,\n            input.uncertainMutation === true,\n          );\n        }\n        if (response.status !== (input.expectedStatus ?? 200)) {\n          await response.body?.cancel().catch(() => undefined);\n          throw new GitHubWebhookRecoveryError(\n            \"github_webhook_recovery_http\",\n            response.status,\n            retryDelay(response.headers),\n            input.uncertainMutation === true &&\n              (response.status >= 500 || response.status === 408),\n          );\n        }\n        return input.project(response, controller.signal);\n      })(),\n    ]);\n  } catch (error) {\n    if (error instanceof GitHubWebhookRecoveryError) throw error;\n    throw new GitHubWebhookRecoveryError(\n      \"github_webhook_recovery_transport\",\n      null,\n      null,\n      input.uncertainMutation === true,\n    );","sourceCodeStart":346,"sourceCodeEnd":382,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-github-webhook-config.ts#L346-L382","documentation":"Thrown when GitHub returned a status code different from the expected one (200 by default, 202 for redelivery, 201 for installation token mint, 204 for token revoke). The error carries the HTTP status, an optional retryAfterMs parsed from Retry-After / x-ratelimit headers, and requestMayHaveBeenAccepted=true only for 5xx/408 on uncertain mutations.","triggerScenarios":"401/403 from an expired or wrong-scope App JWT; 404 from a deleted delivery or installation; 422 from a bad repository list in the token request; 429 rate limit (retryAfterMs set from x-ratelimit-reset); 5xx GitHub server errors; 408 request timeout; redelivery POST returning anything but 202.","commonSituations":"App JWT expired (JWTs live ~10 minutes); installation uninstalled; App lacking webhook read permissions; hitting the 15,000 req/hr App rate limit in recovery loops; GitHub incident causing 5xx.","solutions":["Read e.statusCode and e.retryAfterMs: wait retryAfterMs before retrying on 429/403-with-ratelimit headers.","On 401, mint a fresh App JWT — yours likely expired (60s–10min TTL); on 403 check App permissions and installation status.","On 404, verify the delivery/installation still exists before retrying.","On 5xx/408 during a redelivery, reconcile first (getGitHubAppWebhookDelivery) because the mutation may have been accepted.","Back off and retry GETs with jitter; stop hammering if 429 persists."],"exampleFix":"// before\nawait listGitHubAppWebhookDeliveries({ fetch, appToken }); // throws on 429\n// after\ntry {\n  return await listGitHubAppWebhookDeliveries({ fetch, appToken });\n} catch (e) {\n  if (e instanceof GitHubWebhookRecoveryError && e.code === \"github_webhook_recovery_http\" && e.statusCode === 429 && e.retryAfterMs) {\n    await sleep(e.retryAfterMs);\n    return await listGitHubAppWebhookDeliveries({ fetch, appToken });\n  }\n  throw e;\n}","handlingStrategy":"retry","validationCode":"null","typeGuard":"function isHttpRecoveryError(e: unknown): e is GitHubWebhookRecoveryError & { statusCode: number; retryAfterMs: number | null } {\n  return e instanceof GitHubWebhookRecoveryError && e.code === \"github_webhook_recovery_http\";\n}","tryCatchPattern":"try {\n  await listGitHubAppWebhookDeliveries({ fetch, appToken });\n} catch (e) {\n  if (e instanceof GitHubWebhookRecoveryError && e.code === \"github_webhook_recovery_http\") {\n    if (e.retryAfterMs) await sleep(e.retryAfterMs); // rate-limited: respect Retry-After / x-ratelimit-reset\n    else if (e.statusCode !== null && e.statusCode >= 500) await sleep(jitteredBackoff());\n    else throw e; // 401/403/404/422 need action, not retry\n  } else throw e;\n}","preventionTips":["Refresh App JWTs before they expire (~10 min TTL)","Track x-ratelimit-remaining yourself during bulk recovery loops","Check App installation status before recovery runs","Distinguish retryable (429/5xx/408) from actionable (401/403/404/422) statuses"],"tags":["http","github-api","rate-limit","webhooks"],"backgroundTag":"http-error-response","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}