Skip to content

fix: handle Stripe API errors in OAuth account connection flow#29247

Closed
calebcgates wants to merge 3 commits intocalcom:mainfrom
calebcgates:fix/stripe-oauth-error-handling
Closed

fix: handle Stripe API errors in OAuth account connection flow#29247
calebcgates wants to merge 3 commits intocalcom:mainfrom
calebcgates:fix/stripe-oauth-error-handling

Conversation

@calebcgates
Copy link
Copy Markdown

@calebcgates calebcgates commented May 1, 2026

What does this PR do?

Adds error handling around two unprotected Stripe API calls in saveStripeAccount() in the Stripe Connect OAuth callback flow. Without try-catch, expired authorization codes or deleted Stripe accounts crash the request with a raw 500 error instead of returning an actionable 400.

  • This is a bug fix for unhandled Stripe exceptions — no prior issue exists for this.

Visual Demo (For contributors especially)

N/A — This is a backend-only error handling fix with no UI changes.

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox. N/A — no documentation changes needed.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  1. No special environment variables needed beyond a working Stripe Connect integration (test mode keys).
  2. Run the unit tests: yarn workspace @calcom/api-v2 jest --testPathPattern "stripe.service.spec" — 8 tests cover all error paths.
  3. To reproduce the bug (before fix): Trigger the Stripe Connect OAuth flow, but use an expired or already-used authorization code. The request crashes with an unhandled StripeInvalidGrantError and returns a 500.
  4. Expected behavior (after fix):
    - Expired/invalid OAuth code → 400 BadRequestException with message: "Invalid or expired Stripe authorization code"
    - Deleted/deactivated Stripe account → 400 BadRequestException with message: "Could not retrieve Stripe account"
    - Unexpected Stripe errors → 500 InternalServerErrorException without leaking raw Stripe error details
  5. Verify type-checking passes: yarn type-check:ci --force completes without errors in the apps/api/v2 package.

References:

The saveStripeAccount method makes two Stripe API calls without
try-catch blocks. If the OAuth authorization code is invalid/expired
or the Stripe account has been deleted, the unhandled StripeError
propagates as a 500 Internal Server Error instead of returning a
meaningful error to the user.

This adds targeted error handling:
- oauth.token(): catches StripeInvalidGrantError for expired/invalid
  authorization codes and returns 400 with actionable message
- accounts.retrieve(): catches StripeInvalidRequestError for
  missing/deleted accounts and returns 400 with clear explanation
- All other Stripe errors surface as 500 with a generic message
  rather than leaking raw Stripe error details
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

Welcome to Cal.diy, @calebcgates! Thanks for opening this pull request.

A few things to keep in mind:

  • This is Cal.diy, not Cal.com. Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will not be deployed to the Cal.com production app.
  • Please review our Contributing Guidelines if you haven't already.
  • Make sure your PR title follows the Conventional Commits format.

A maintainer will review your PR soon. Thanks for contributing!

…ount

Covers all error paths introduced in the Stripe Connect OAuth callback:
- StripeInvalidGrantError → BadRequestException (expired/invalid code)
- StripeInvalidRequestError → BadRequestException (deleted account)
- Unexpected errors → InternalServerErrorException
- Happy path with and without stripe_user_id
- Credential cleanup before creating new ones
@pull-request-size pull-request-size Bot added size/L and removed size/S labels May 1, 2026
@calebcgates calebcgates marked this pull request as ready for review May 1, 2026 16:57
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 2026

📝 Walkthrough

Walkthrough

A test suite for the StripeService.saveStripeAccount method is added, verifying authorization failures throw UnauthorizedException, successful OAuth exchanges return the expected response and invoke the token endpoint, missing accounts and invalid grants map to BadRequestException, and unexpected failures result in InternalServerErrorException. The implementation is updated to wrap OAuth token exchange and account retrieval in explicit error handling that converts Stripe-specific errors to appropriate HTTP exceptions, while maintaining existing credential deletion and new credential creation logic.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: handle Stripe API errors in OAuth account connection flow' directly and clearly describes the main change: adding error handling for Stripe API calls in the OAuth flow.
Description check ✅ Passed The description comprehensively explains the bug fix, including what was fixed, how to test it, expected behavior, and references to relevant documentation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

Copy link
Copy Markdown
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

🧹 Nitpick comments (1)
apps/api/v2/src/modules/stripe/stripe.service.spec.ts (1)

135-176: ⚡ Quick win

Avoid calling saveStripeAccount twice in each rejection test.

These tests currently execute the method twice to assert type and message. Use a single rejection instance per test to reduce brittleness and accidental side effects in future changes.

Suggested pattern
-      await expect(service.saveStripeAccount(mockState, "expired_code", 1)).rejects.toThrow(BadRequestException);
-      await expect(service.saveStripeAccount(mockState, "expired_code", 1)).rejects.toThrow(
-        "Invalid or expired Stripe authorization code"
-      );
+      const promise = service.saveStripeAccount(mockState, "expired_code", 1);
+      await expect(promise).rejects.toThrow(BadRequestException);
+      await expect(promise).rejects.toThrow("Invalid or expired Stripe authorization code");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/v2/src/modules/stripe/stripe.service.spec.ts` around lines 135 -
176, Tests call service.saveStripeAccount twice per rejection case which can
cause side effects; capture the single rejected promise returned by
saveStripeAccount (e.g., const promise = service.saveStripeAccount(...)) and
assert both the error type and message against that promise instead of invoking
saveStripeAccount twice. Apply this change to each failing test that currently
calls saveStripeAccount twice (those using mockOAuthToken and
mockAccountsRetrieve setups) so assertions check the same rejection instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/v2/src/modules/stripe/stripe.service.ts`:
- Around line 105-110: Add an early guard that validates the OAuth `code` before
calling stripeInstance.oauth.token: check the `code` input (used where
code?.toString() is passed to stripeInstance.oauth.token) and if it's
null/undefined/empty, return or throw a client error (HTTP 400) with a clear
message instead of proceeding to call stripeInstance.oauth.token; only call
stripeInstance.oauth.token with the confirmed non-empty code (e.g., use
code.toString() after the guard).

---

Nitpick comments:
In `@apps/api/v2/src/modules/stripe/stripe.service.spec.ts`:
- Around line 135-176: Tests call service.saveStripeAccount twice per rejection
case which can cause side effects; capture the single rejected promise returned
by saveStripeAccount (e.g., const promise = service.saveStripeAccount(...)) and
assert both the error type and message against that promise instead of invoking
saveStripeAccount twice. Apply this change to each failing test that currently
calls saveStripeAccount twice (those using mockOAuthToken and
mockAccountsRetrieve setups) so assertions check the same rejection instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2e24084e-d4a2-440d-9c14-25e52dc73889

📥 Commits

Reviewing files that changed from the base of the PR and between d278d6c and 73c0ad9.

📒 Files selected for processing (2)
  • apps/api/v2/src/modules/stripe/stripe.service.spec.ts
  • apps/api/v2/src/modules/stripe/stripe.service.ts

Comment on lines +105 to +110
let response;
try {
response = await stripeInstance.oauth.token({
grant_type: "authorization_code",
code: code?.toString(),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add an early guard for missing OAuth code before calling Stripe.

Line [109] allows an empty/undefined code to reach oauth.token, which then falls into the 500 path. This should fail fast as a client error (400) before the external call.

Suggested fix
   async saveStripeAccount(state: OAuthCallbackState, code: string, userId: number): Promise<{ url: string }> {
     if (!userId) {
       throw new UnauthorizedException("Invalid Access token.");
     }
+    if (!code?.trim()) {
+      throw new BadRequestException("Missing Stripe authorization code.");
+    }
 
     let response;
     try {
       response = await stripeInstance.oauth.token({
         grant_type: "authorization_code",
-        code: code?.toString(),
+        code,
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/v2/src/modules/stripe/stripe.service.ts` around lines 105 - 110, Add
an early guard that validates the OAuth `code` before calling
stripeInstance.oauth.token: check the `code` input (used where code?.toString()
is passed to stripeInstance.oauth.token) and if it's null/undefined/empty,
return or throw a client error (HTTP 400) with a clear message instead of
proceeding to call stripeInstance.oauth.token; only call
stripeInstance.oauth.token with the confirmed non-empty code (e.g., use
code.toString() after the guard).

@romitg2
Copy link
Copy Markdown
Member

romitg2 commented May 3, 2026

please reopen with video demo attached

@romitg2 romitg2 closed this May 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants