* Send an authenticated HTTP request to CCR. Handles auth headers, * 409 epoch mismatch, and error logging. Returns { ok: true } on 2xx. * On 429, reads Retry-After (integer seconds) so the uploader can honor * the server's backoff hint instead of blindly exponentiating.
(
method: 'post' | 'put',
path: string,
body: unknown,
label: string,
{ timeout = 10_000 }: { timeout?: number } = {},
)
| 554 | * the server's backoff hint instead of blindly exponentiating. |
| 555 | */ |
| 556 | private async request( |
| 557 | method: 'post' | 'put', |
| 558 | path: string, |
| 559 | body: unknown, |
| 560 | label: string, |
| 561 | { timeout = 10_000 }: { timeout?: number } = {}, |
| 562 | ): Promise<RequestResult> { |
| 563 | const authHeaders = this.getAuthHeaders() |
| 564 | if (Object.keys(authHeaders).length === 0) return { ok: false } |
| 565 | |
| 566 | try { |
| 567 | const response = await this.http[method]( |
| 568 | `${this.sessionBaseUrl}${path}`, |
| 569 | body, |
| 570 | { |
| 571 | headers: { |
| 572 | ...authHeaders, |
| 573 | 'Content-Type': 'application/json', |
| 574 | 'anthropic-version': '2023-06-01', |
| 575 | 'User-Agent': getClaudeCodeUserAgent(), |
| 576 | }, |
| 577 | validateStatus: alwaysValidStatus, |
| 578 | timeout, |
| 579 | }, |
| 580 | ) |
| 581 | |
| 582 | if (response.status >= 200 && response.status < 300) { |
| 583 | this.consecutiveAuthFailures = 0 |
| 584 | return { ok: true } |
| 585 | } |
| 586 | if (response.status === 409) { |
| 587 | this.handleEpochMismatch() |
| 588 | } |
| 589 | if (response.status === 401 || response.status === 403) { |
| 590 | // A 401 with an expired JWT is deterministic — no retry will |
| 591 | // ever succeed. Check the token's own exp before burning |
| 592 | // wall-clock on the threshold loop. |
| 593 | const tok = getSessionIngressAuthToken() |
| 594 | const exp = tok ? decodeJwtExpiry(tok) : null |
| 595 | if (exp !== null && exp * 1000 < Date.now()) { |
| 596 | logForDebugging( |
| 597 | `CCRClient: session_token expired (exp=${new Date(exp * 1000).toISOString()}) — no refresh was delivered, exiting`, |
| 598 | { level: 'error' }, |
| 599 | ) |
| 600 | logForDiagnosticsNoPII('error', 'cli_worker_token_expired_no_refresh') |
| 601 | this.onEpochMismatch() |
| 602 | } |
| 603 | // Token looks valid but server says 401 — possible server-side |
| 604 | // blip (userauth down, KMS hiccup). Count toward threshold. |
| 605 | this.consecutiveAuthFailures++ |
| 606 | if (this.consecutiveAuthFailures >= MAX_CONSECUTIVE_AUTH_FAILURES) { |
| 607 | logForDebugging( |
| 608 | `CCRClient: ${this.consecutiveAuthFailures} consecutive auth failures with a valid-looking token — server-side auth unrecoverable, exiting`, |
| 609 | { level: 'error' }, |
| 610 | ) |
| 611 | logForDiagnosticsNoPII('error', 'cli_worker_auth_failures_exhausted') |
| 612 | this.onEpochMismatch() |
| 613 | } |
no test coverage detected