Skip to content

fix: wrap API calls in timeout - #143

Open
cmatheson wants to merge 1 commit into
mainfrom
fix/refresh-request-timeout
Open

cmatheson wants to merge 1 commit into
mainfrom
fix/refresh-request-timeout

Conversation

@cmatheson

Copy link
Copy Markdown
Collaborator

"Stuck" requests could hold the cross-tab refresh lock indefinitely (preventing thos OR other tabs from refreshing). This aborts API calls after 8s with a non-terminal error.

Fixes #140

@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 2/5

[Medium risk] Adds request timeout handling to the HTTP client.

The PR does not appear safe to merge while the previously reported refresh and callback failures remain.

Findings

  1. P1 Request abort mistaken for lock timeout ▶
  2. P1 Valid token rejected after timeout ▶
  3. P1 Slow code exchanges lose callbacks ▶
Fix with agent prompt
### Issue 1
src/http-client.ts:undefined-139
If a refresh request times out after acquiring a native Web Lock, its `AbortError` passes through the lock callback and is classified as a lock-acquisition timeout. `switchToOrganization()` then logs a warning and resolves even though the switch failed. `getAccessToken()` also retries the failed request as though it had only been waiting for the lock.

### Issue 2
src/http-client.ts:undefined-139
A non-forced token request can start a proactive refresh while its current access token is still valid. If that refresh hits the new timeout on the fallback-lock path, `getAccessToken()` rethrows the `AbortError` instead of returning the valid token, so a serviceable authenticated call fails.

### Issue 3
src/http-client.ts:undefined-104
The eight-second limit chosen for refresh-lock coordination also applies to authorization-code exchange, which does not hold that lock. If a legitimate exchange takes longer, the request is aborted, callback handling marks authentication as failed, and cleanup removes the code and stored verifier. The user cannot complete that callback even if the service was about to respond successfully.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR adds an eight-second abort timeout to refresh and authorization-code API calls, including response-body reads, and adds tests for requests that do not settle. There have been no code changes since the previous review.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Client[AuthKit client] --> Refresh[Refresh request]
  Client --> Exchange[Authorization-code exchange]
  Refresh --> Lock[Cross-tab refresh lock]
  Lock --> Timeout[Eight-second request timeout]
  Exchange --> Timeout
  Timeout --> Abort[Abort fetch or body read]
Loading

Reviews (3) · Last reviewed commit: "fix: wrap refresh calls in timeout"

Comment thread src/http-client.ts
request: (signal: AbortSignal) => Promise<T>,
): Promise<T> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Request abort mistaken for lock timeout

If a refresh request times out after acquiring a native Web Lock, its AbortError passes through the lock callback and is classified as a lock-acquisition timeout. switchToOrganization() then logs a warning and resolves even though the switch failed. getAccessToken() also retries the failed request as though it had only been waiting for the lock.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/http-client.ts
Line: 139

Comment:
**Request abort mistaken for lock timeout**

If a refresh request times out after acquiring a native Web Lock, its `AbortError` passes through the lock callback and is classified as a lock-acquisition timeout. `switchToOrganization()` then logs a warning and resolves even though the switch failed. `getAccessToken()` also retries the failed request as though it had only been waiting for the lock.

**Knowledge Base Used:**
- [Session storage and tab locking](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/authkit-js/-/docs/session-storage-and-locking.md)
- [HTTP API client](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/authkit-js/-/docs/http-api-client.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread src/http-client.ts
request: (signal: AbortSignal) => Promise<T>,
): Promise<T> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Valid token rejected after timeout

A non-forced token request can start a proactive refresh while its current access token is still valid. If that refresh hits the new timeout on the fallback-lock path, getAccessToken() rethrows the AbortError instead of returning the valid token, so a serviceable authenticated call fails.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/http-client.ts
Line: 139

Comment:
**Valid token rejected after timeout**

A non-forced token request can start a proactive refresh while its current access token is still valid. If that refresh hits the new timeout on the fallback-lock path, `getAccessToken()` rethrows the `AbortError` instead of returning the valid token, so a serviceable authenticated call fails.

**Knowledge Base Used:**
- [HTTP API client](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/authkit-js/-/docs/http-api-client.md)
- [Client error contracts](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/authkit-js/-/docs/error-contracts.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread src/http-client.ts
grant_type: "authorization_code",
code_verifier: codeVerifier,
},
return this.#withTimeout(async (signal) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Slow code exchanges lose callbacks

The eight-second limit chosen for refresh-lock coordination also applies to authorization-code exchange, which does not hold that lock. If a legitimate exchange takes longer, the request is aborted, callback handling marks authentication as failed, and cleanup removes the code and stored verifier. The user cannot complete that callback even if the service was about to respond successfully.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/http-client.ts
Line: 104

Comment:
**Slow code exchanges lose callbacks**

The eight-second limit chosen for refresh-lock coordination also applies to authorization-code exchange, which does not hold that lock. If a legitimate exchange takes longer, the request is aborted, callback handling marks authentication as failed, and cleanup removes the code and stored verifier. The user cannot complete that callback even if the service was about to respond successfully.

**Knowledge Base Used:**
- [Authorization redirects and callbacks](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/authkit-js/-/docs/authorization-and-callbacks.md)
- [Client error contracts](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/authkit-js/-/docs/error-contracts.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

"Stuck" requests could hold the cross-tab refresh lock indefinitely
(preventing thos OR other tabs from refreshing). This aborts API calls
after 8s with a non-terminal error.

Fixes #140
@cmatheson
cmatheson force-pushed the fix/refresh-request-timeout branch from f98f90e to 32aecde Compare September 30, 2026 15:12
@cmatheson

Copy link
Copy Markdown
Collaborator Author

@greptile-apps update

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Refresh request has no timeout, so a hung fetch holds the cross-tab refresh lock

1 participant