Skip to content

Fix error toast on connect account#972

Merged
elie222 merged 5 commits intomainfrom
fix/callback-error-toaster
Nov 16, 2025
Merged

Fix error toast on connect account#972
elie222 merged 5 commits intomainfrom
fix/callback-error-toaster

Conversation

@elie222
Copy link
Owner

@elie222 elie222 commented Nov 15, 2025

Summary by CodeRabbit

  • Refactor

    • Account linking now uses a server-side cache and distributed locking to reliably handle duplicate OAuth callbacks and simplify state handling.
    • Reduced reliance on temporary result cookies; success flows consistently redirect users to the Accounts page with explicit success parameters.
  • Bug Fixes

    • Better handling of concurrent callbacks, duplicate accounts, and error cleanup to avoid stale state and confusing redirects.

@vercel
Copy link

vercel bot commented Nov 15, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
inbox-zero Ready Ready Preview Nov 16, 2025 7:52am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 15, 2025

Walkthrough

Replaces cookie-based OAuth callback deduplication with a Redis-backed per-code lock and 60s result cache. Callback routes short-circuit on cached results, acquire per-code locks to prevent concurrent processing, persist success results, clear Redis entries on errors, and remove several cookie-based helpers/constants.

Changes

Cohort / File(s) Summary
OAuth callback routes
apps/web/app/api/google/linking/callback/route.ts, apps/web/app/api/outlook/linking/callback/route.ts
Replace cookie-result dedupe with Redis-backed per-code lock and 60s result cache. Add cache-hit short-circuit, acquire lock before processing, persist success results via Redis, replace helper-driven redirects with direct /accounts redirects, handle duplicate-account create errors by resolving to existing account when appropriate, and clear Redis entries on errors.
Removed OAuth callback utilities
apps/web/utils/oauth/callback-helpers.ts
Delete checkOAuthCallbackDedupe, buildOAuthSuccessRedirect and associated types — remove cookie-result-based dedupe and redirect helpers.
OAuth state utilities cleanup
apps/web/utils/oauth/state.ts
Remove OAuthStateResultCookieValue and encode/parse helpers for the state-result cookie; keep remaining state generation/parsing and cookie options.
OAuth error handler update
apps/web/utils/oauth/error-handler.ts
Remove resultCookieName from ErrorHandlerParams and handleOAuthCallbackError; stop deleting result cookie and add error=link_failed into error redirect params.
Result cookie constants removal
apps/web/utils/gmail/constants.ts, apps/web/utils/outlook/constants.ts
Remove exported GOOGLE_LINKING_STATE_RESULT_COOKIE_NAME and OUTLOOK_LINKING_STATE_RESULT_COOKIE_NAME.
New Redis OAuth code module
apps/web/utils/redis/oauth-code.ts
Add Redis-backed OAuth code utilities: hashed keys oauth-code:<hash>, 60s TTL; exports acquireOAuthCodeLock, getOAuthCodeResult, setOAuthCodeResult, clearOAuthCode, and an internal OAuthCodeResult shape.
Auth URL handlers
apps/web/app/api/google/linking/auth-url/route.ts, apps/web/app/api/outlook/linking/auth-url/route.ts
Stop referencing/deleting removed result cookie constants; continue setting the state cookie and building auth URL as before.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Provider as Google/Outlook
    participant Callback as Callback Route
    participant Redis
    participant Accounts

    rect rgb(240,250,240)
    Note over Callback,Redis: Per-code lock + 60s result cache
    end

    Client->>Provider: OAuth flow -> redirect with code
    Client->>Callback: GET /callback?code=CODE

    Callback->>Redis: getOAuthCodeResult(CODE)
    alt Cache Hit (success)
        Redis-->>Callback: {status:"success", params}
        Callback-->>Client: Redirect /accounts?success=1&params...
    else Cache Miss
        Redis-->>Callback: null
        Callback->>Redis: acquireOAuthCodeLock(CODE)
        alt Lock Acquired
            Redis-->>Callback: true
            Callback->>Provider: Exchange CODE for token
            Provider-->>Callback: token + user info
            Callback->>Callback: create or merge account
            Callback->>Redis: setOAuthCodeResult(CODE, params)
            Redis-->>Callback: ok
            Callback-->>Client: Redirect /accounts?success=1&params...
        else Lock Not Acquired
            Redis-->>Callback: false
            Callback-->>Client: Redirect /accounts (short-circuit)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • Correctness and atomicity of Redis lock (NX + TTL) and result storage semantics in apps/web/utils/redis/oauth-code.ts.
    • All early-return paths in both callback routes — cache-hit, lock-failed, and error handling — properly clear state cookies and/or Redis entries.
    • Duplicate-account handling during creation: ensure behavior is correct when resolving to an existing account.
    • Ensure no remaining references to removed constants/helpers (GOOGLE_LINKING_STATE_RESULT_COOKIE_NAME, OUTLOOK_LINKING_STATE_RESULT_COOKIE_NAME, and callback-helpers).

Possibly related PRs

Poem

🐰 I hopped where cookies used to hide,
Found Redis tunnels deep inside,
Locks snug for sixty ticks,
No duplicate magic tricks,
My code-burrow sleeps satisfied 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Fix error toast on connect account' is vague and does not clearly describe the substantial changes in this PR, which involve replacing cookie-based OAuth deduplication with Redis-backed OAuth code caching and locking across multiple OAuth callback handlers. Consider a more descriptive title that reflects the main architectural change, such as 'Replace cookie-based OAuth deduplication with Redis code cache' or 'Refactor OAuth callback handling to use Redis-backed locking'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/callback-error-toaster

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (6)
apps/web/utils/redis/oauth-code.ts (2)

4-10: Hashing choice and CodeQL warning look acceptable; consider documenting intent

Using a single sha256 and truncating to 16 hex chars for the Redis key is fine here since the OAuth code is short‑lived, only used to derive a cache key, and expires after 60s. The CodeQL “password hash with insufficient computational effort” warning appears to be a false positive in this context. You might add a brief comment that this hash is for keying/deduplication only (not password storage) to avoid future confusion.


17-24: Lock/result semantics are sound; small edge case around non-success flows

The pattern of:

  • SET key "processing" NX EX 60 as a lock, and
  • overwriting the same key with the success payload (also EX 60) for dedupe

is a good fit for preventing duplicate processing and serving cached success responses.

One minor edge case: flows that return early without calling setOAuthCodeResult (e.g., if upstream callers exit on a redirect‑type linking result) will leave the key at "processing" until TTL expiry, causing subsequent callbacks for that code to hit the “being processed” path rather than a cached result. If you expect duplicate callbacks in those paths and want consistent behavior, consider either clearing the key or also caching a “non-success” result there.

Also applies to: 26-40, 42-55

apps/web/app/api/google/linking/callback/route.ts (2)

39-64: Redis-backed dedupe logic looks correct; note behavior of second callbacks during in-flight processing

The sequence:

  1. Try getOAuthCodeResult(code) and, on hit, replay the original success params.
  2. Otherwise attempt acquireOAuthCodeLock(code), and if it fails, redirect to /accounts and clear the state cookie.

is a solid way to avoid duplicate processing and should prevent spurious error toasts from second callbacks using the same code. One UX nuance: in the lock-fail path the redirect carries no success or error query, so the user just ends up on /accounts with a neutral state. If you want the same success toast on concurrent callbacks, you might consider a short polling window or another mechanism to reuse the cached result instead of immediately bailing.


172-179: Success handling duplicated; consider a small helper for consistency

Both the “new account created” and “merge” branches:

  • call setOAuthCodeResult(code, { success: ... }),
  • build /accounts?success=..., and
  • delete GOOGLE_LINKING_STATE_COOKIE_NAME before redirecting.

This pattern is also mirrored in the Outlook callback. A tiny helper (e.g. something like redirectWithOAuthSuccess({ request, code, success, stateCookieName })) would reduce duplication and make it harder for the flows to drift apart in future changes.

Also applies to: 210-217

apps/web/app/api/outlook/linking/callback/route.ts (2)

42-67: Outlook callback dedupe mirrors Google logic correctly

The cache-first, then lock acquisition flow matches the Google handler and should similarly avoid duplicate processing and spurious errors for reused codes. As with the Google route, the lock-fail path redirects to /accounts without a success/error query, which keeps things neutral but doesn’t replay the original success toast; if you want identical UX for concurrent callbacks, you could consider a small polling or reuse mechanism instead of a bare redirect.


243-250: Success redirect logic is duplicated and could be shared

The “create” and “merge” branches both:

  • setOAuthCodeResult(code, { success: ... }),
  • build /accounts?success=..., and
  • delete OUTLOOK_LINKING_STATE_COOKIE_NAME before redirecting.

Given this is almost identical to the Google route, a shared helper for “write success to Redis and build the standard /accounts redirect” would reduce repeated code and keep provider behavior aligned.

Also applies to: 279-286

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c543ff0 and 2405db8.

📒 Files selected for processing (8)
  • apps/web/app/api/google/linking/callback/route.ts (4 hunks)
  • apps/web/app/api/outlook/linking/callback/route.ts (4 hunks)
  • apps/web/utils/gmail/constants.ts (0 hunks)
  • apps/web/utils/oauth/callback-helpers.ts (0 hunks)
  • apps/web/utils/oauth/error-handler.ts (0 hunks)
  • apps/web/utils/oauth/state.ts (0 hunks)
  • apps/web/utils/outlook/constants.ts (0 hunks)
  • apps/web/utils/redis/oauth-code.ts (1 hunks)
💤 Files with no reviewable changes (5)
  • apps/web/utils/gmail/constants.ts
  • apps/web/utils/outlook/constants.ts
  • apps/web/utils/oauth/state.ts
  • apps/web/utils/oauth/error-handler.ts
  • apps/web/utils/oauth/callback-helpers.ts
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/app/api/google/linking/callback/route.ts (3)
apps/web/utils/redis/oauth-code.ts (4)
  • getOAuthCodeResult (26-40)
  • acquireOAuthCodeLock (17-24)
  • setOAuthCodeResult (42-52)
  • clearOAuthCode (54-56)
apps/web/utils/gmail/constants.ts (1)
  • GOOGLE_LINKING_STATE_COOKIE_NAME (16-16)
apps/web/utils/oauth/error-handler.ts (1)
  • handleOAuthCallbackError (11-25)
apps/web/app/api/outlook/linking/callback/route.ts (3)
apps/web/utils/redis/oauth-code.ts (4)
  • getOAuthCodeResult (26-40)
  • acquireOAuthCodeLock (17-24)
  • setOAuthCodeResult (42-52)
  • clearOAuthCode (54-56)
apps/web/utils/outlook/constants.ts (1)
  • OUTLOOK_LINKING_STATE_COOKIE_NAME (1-1)
apps/web/utils/oauth/error-handler.ts (1)
  • handleOAuthCallbackError (11-25)
🪛 GitHub Check: CodeQL
apps/web/utils/redis/oauth-code.ts

[failure] 5-5: Use of password hash with insufficient computational effort
Password from a call to validateOAuthCallback is hashed insecurely.
Password from a call to validateOAuthCallback is hashed insecurely.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: test
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
apps/web/app/api/google/linking/callback/route.ts (1)

219-227: Good: lock cleanup on error path

Clearing the OAuth code key in the catch block before delegating to handleOAuthCallbackError ensures you don’t leave the “processing” sentinel stuck in Redis after errors, which would otherwise block future attempts for up to the TTL.

apps/web/app/api/outlook/linking/callback/route.ts (1)

288-296: Good: clearing OAuth code on error prevents stale locks

Calling clearOAuthCode(code) in the catch block before delegating to handleOAuthCallbackError avoids leaving a “processing” value hanging in Redis after failures, which keeps future attempts from being blocked by a stale lock.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 8 files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2405db8 and d10ad96.

📒 Files selected for processing (1)
  • apps/web/utils/redis/oauth-code.ts (1 hunks)
🧰 Additional context used
🪛 GitHub Check: CodeQL
apps/web/utils/redis/oauth-code.ts

[failure] 6-6: Use of password hash with insufficient computational effort
Password from a call to validateOAuthCallback is hashed insecurely.
Password from a call to validateOAuthCallback is hashed insecurely.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: test
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
apps/web/utils/redis/oauth-code.ts (2)

4-7: Static analysis false positive - implementation is appropriate.

The CodeQL warning about "insecure password hashing" is a false positive. This function generates cache keys for short-lived OAuth authorization codes, not password hashes. The comment on line 4 correctly clarifies this distinction.

The truncation to 16 hex characters (64 bits) provides sufficient collision resistance for the use case: OAuth codes that live only 60 seconds and are randomly generated.


18-25: LGTM! Solid distributed lock and cache implementation.

The lock acquisition using Redis SET with the NX flag provides atomic semantics, preventing race conditions. The consistent 60-second TTL across lock and result storage ensures proper cleanup. The clear/delete operation completes the lifecycle management.

Also applies to: 43-57

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
apps/web/app/api/google/linking/callback/route.ts (1)

56-65: Consider adding user feedback when lock acquisition fails.

When another request is already processing the OAuth code, the user is redirected to /accounts without any query parameters. While this prevents duplicate processing, the user receives no feedback about what happened or whether the linking succeeded.

Consider adding a query parameter (e.g., ?info=processing or ?success=pending) so the UI can inform the user that their request is being processed.

     const redirectUrl = new URL("/accounts", request.nextUrl.origin);
+    redirectUrl.searchParams.set("info", "already_processing");
     const response = NextResponse.redirect(redirectUrl);
apps/web/app/api/outlook/linking/callback/route.ts (3)

43-68: Redis dedupe and lock flow is solid; consider UX for lock contention path

The getOAuthCodeResult + acquireOAuthCodeLock sequence correctly ensures:

  • Finished callbacks short‑circuit using cached params, and
  • Only one request per code actually does the heavy work.

However, when the lock isn’t acquired, the handler immediately redirects to /accounts without any success or error query params. Depending on how the /accounts page decides to show the “connect failed” toast, this “lock contention” path might still end up surfacing an error even though a concurrent request is legitimately processing the link.

If the frontend currently treats “no success param” as a failure, consider:

  • Either making the losing request wait briefly and re-check getOAuthCodeResult before redirecting, or
  • Agreeing on a neutral flag (or success reuse) that prevents a spurious error toast in this branch.

This is more of a UX/flow refinement than a correctness bug, but worth double‑checking against the /accounts logic.


212-281: Account creation + duplicate handling look correct; minor clarity improvements possible

The new microsoftProviderAccountId const and the create block wrapped in try/catch with isDuplicateError give you robust handling for concurrent account creation:

  • You correctly reuse profile.id || providerEmail as the providerAccountId and check for a concurrent create by re‑querying and comparing userId to targetUserId.
  • On true concurrency, you log and continue, and on any other duplicate you rethrow, which is the right behavior.

Two small refinements you might consider:

  1. Narrow the duplicate check explicitly to the provider/providerAccountId index for readability

    Even though the follow-up findUnique guard already makes this effectively safe, making intent explicit would help future readers:

  •  } catch (createError: unknown) {
    
  •    if (isDuplicateError(createError)) {
    
  •  } catch (createError: unknown) {
    
  •    if (isDuplicateError(createError, "provider_providerAccountId")) {
    
    
    
  1. Avoid duplicating the providerAccountId expression

    The expression profile.id || providerEmail is used both in the earlier findUnique and here via microsoftProviderAccountId. Hoisting this to a single const (right after you compute providerEmail) and reusing it in both places would reduce the risk of the two paths diverging in future edits.

Neither of these is blocking, but they would tighten up intent and maintainability.


319-325: Consider not surfacing cache/Redis failures as user-visible link failures

The catch block currently wraps the entire processing pipeline. That means if setOAuthCodeResult (or a later Redis call) throws after the account has been successfully created/merged in Prisma, the user will be redirected with error=link_failed even though their Microsoft account is actually linked.

If you want to avoid “false negative” toasts in that situation, you could:

  • Wrap the Redis interactions (setOAuthCodeResult, clearOAuthCode) in their own internal try/catch, logging failures but not changing the user-facing outcome once the DB work has succeeded.
  • Keep the outer catch primarily for real linking failures (token exchange, profile fetch, Prisma errors).

That would preserve dedupe behavior when Redis is healthy but avoid telling users their link failed when it actually succeeded.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef1cb90 and 44d9d8c.

📒 Files selected for processing (2)
  • apps/web/app/api/google/linking/callback/route.ts (4 hunks)
  • apps/web/app/api/outlook/linking/callback/route.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/app/api/outlook/linking/callback/route.ts (4)
apps/web/utils/redis/oauth-code.ts (4)
  • getOAuthCodeResult (27-41)
  • acquireOAuthCodeLock (18-25)
  • setOAuthCodeResult (43-53)
  • clearOAuthCode (55-57)
apps/web/utils/outlook/constants.ts (1)
  • OUTLOOK_LINKING_STATE_COOKIE_NAME (1-1)
apps/web/utils/prisma-helpers.ts (1)
  • isDuplicateError (3-12)
apps/web/utils/oauth/error-handler.ts (1)
  • handleOAuthCallbackError (11-25)
apps/web/app/api/google/linking/callback/route.ts (4)
apps/web/utils/redis/oauth-code.ts (4)
  • getOAuthCodeResult (27-41)
  • acquireOAuthCodeLock (18-25)
  • setOAuthCodeResult (43-53)
  • clearOAuthCode (55-57)
apps/web/utils/gmail/constants.ts (1)
  • GOOGLE_LINKING_STATE_COOKIE_NAME (16-16)
apps/web/utils/prisma-helpers.ts (1)
  • isDuplicateError (3-12)
apps/web/utils/oauth/error-handler.ts (1)
  • handleOAuthCallbackError (11-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (9)
apps/web/app/api/google/linking/callback/route.ts (7)

1-17: LGTM! Imports are clean and appropriate.

The new Redis-backed OAuth utilities and duplicate error helper are properly imported and support the deduplication strategy.


40-54: Excellent cache-hit optimization.

The cached result check prevents redundant processing of OAuth codes that have already been handled. The reconstruction of redirect parameters and cookie cleanup are correct.


144-201: Excellent race condition handling for concurrent account creation.

The duplicate error handling correctly addresses the scenario where concurrent requests attempt to create the same Google account. The logic properly validates that the duplicate account belongs to the target user before continuing.

The edge case where accountNow is null (account created then immediately deleted) is handled by rethrowing the error, which is acceptable for this unlikely scenario.


203-210: LGTM! Success result caching and redirect are correct.

Storing the success result in Redis enables efficient handling of duplicate callback requests, and the redirect properly communicates the outcome to the user.


241-248: LGTM! Consistent success handling pattern.

The account merge path follows the same caching and redirect pattern as account creation, ensuring consistent behavior across both flows.


249-259: Critical: Error path correctly clears Redis state.

Clearing the OAuth code from Redis on error (line 250) is essential—it ensures that failed attempts are not cached and allows users to retry the OAuth flow. The error handling flow is correct.


19-260: Robust deduplication architecture.

The Redis-backed locking and caching strategy effectively prevents race conditions and duplicate OAuth callback processing:

  • Per-code locks prevent concurrent processing
  • 60-second TTLs provide automatic cleanup
  • Cache hits short-circuit redundant requests
  • Duplicate error handling addresses concurrent account creation
  • Error paths properly clean up Redis state

The implementation is defensive and handles edge cases well.

apps/web/app/api/outlook/linking/callback/route.ts (2)

1-18: Imports correctly wire in state cookie, Redis helpers, and duplicate detection

The new imports line up with later usage (OUTLOOK_LINKING_STATE_COOKIE_NAME, Redis OAuth helpers, isDuplicateError) and keep concerns nicely factored into shared utilities. No issues here.


274-281: Success result caching and redirects are consistent and support deduped callbacks

Setting the OAuth code result in Redis and then redirecting with a success query param in both the “new account created” and “merge” paths is a good match for the early cachedResult branch. Subsequent callbacks for the same code will reuse the exact same params, and every success path consistently clears the state cookie on the outgoing response.

This should address repeated callback behavior and align with the PR’s goal of avoiding spurious error toasts on successful links.

Also applies to: 310-317

@elie222 elie222 merged commit df10093 into main Nov 16, 2025
15 checks passed
@elie222 elie222 deleted the fix/callback-error-toaster branch November 16, 2025 07:58
@coderabbitai coderabbitai bot mentioned this pull request Jan 7, 2026
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.

1 participant

Comments