Skip to content

fix: use .catch() instead of .default() for period validation in checkout#3972

Merged
yujonglee merged 1 commit intomainfrom
devin/1771075009-fix-checkout-period-validation
Feb 14, 2026
Merged

fix: use .catch() instead of .default() for period validation in checkout#3972
yujonglee merged 1 commit intomainfrom
devin/1771075009-fix-checkout-period-validation

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Feb 14, 2026

Summary

Fixes a Sentry error (HYPRNOTE2-6TA) where Safari 26.2 sends period=monthly/ (with a trailing slash from URL normalization), causing the Zod enum validation to throw since .default() only handles undefined values, not invalid strings.

Changed .default("monthly").catch("monthly") so any invalid period value gracefully falls back to "monthly" instead of throwing a validation error that cascades into React concurrent rendering recovery errors.

Review & Testing Checklist for Human

  • Verify .catch() silently falling back to "monthly" for any bad value (including period=yearly/ → becomes monthly) is acceptable behavior, rather than attempting to strip/normalize the value
  • Check if scheme: z.enum(VALID_SCHEMES).optional() has the same trailing-slash vulnerability — an invalid scheme value like hyprnote/ would also throw rather than becoming undefined

Notes


Open with Devin

…kout

.default() only applies when value is undefined, but .catch() handles
any invalid value (e.g. 'monthly/' with trailing slash from Safari URL
handling) by falling back to 'monthly'.

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
@netlify
Copy link

netlify bot commented Feb 14, 2026

Deploy Preview for hyprnote-storybook canceled.

Name Link
🔨 Latest commit bb67b28
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/6990768c650e34000817ad13

@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@netlify
Copy link

netlify bot commented Feb 14, 2026

Deploy Preview for hyprnote ready!

Name Link
🔨 Latest commit bb67b28
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/6990768c5de26300082fcdb1
😎 Deploy Preview https://deploy-preview-3972--hyprnote.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@yujonglee yujonglee merged commit ee857cb into main Feb 14, 2026
12 of 14 checks passed
@yujonglee yujonglee deleted the devin/1771075009-fix-checkout-period-validation branch February 14, 2026 13:21
Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Open in Devin Review

const validateSearch = z.object({
period: z.enum(["monthly", "yearly"]).default("monthly"),
period: z.enum(["monthly", "yearly"]).catch("monthly"),
scheme: z.enum(VALID_SCHEMES).optional(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🔴 scheme field has the same trailing-slash vulnerability that was just fixed for period

The scheme field on line 15 uses .optional() which only handles undefined values, not invalid strings. If Safari (or any browser) sends a URL like ?scheme=hyprnote/ (with a trailing slash from URL normalization — the exact same issue that motivated this PR for period), the Zod enum validation will throw instead of gracefully falling back to undefined.

Root Cause and Suggested Fix

The period field was changed from .default("monthly") to .catch("monthly") to handle invalid values like monthly/. The scheme field at apps/web/src/routes/_view/app/checkout.tsx:15 has the identical vulnerability:

scheme: z.enum(VALID_SCHEMES).optional(),  // throws on "hyprnote/"

Should be:

scheme: z.enum(VALID_SCHEMES).optional().catch(undefined),  // falls back to undefined on invalid values

Impact: An invalid scheme value (e.g. with a trailing slash) will cause a Zod validation error, which cascades into the same kind of React concurrent rendering recovery error described in the Sentry issue HYPRNOTE2-6TA.

Suggested change
scheme: z.enum(VALID_SCHEMES).optional(),
scheme: z.enum(VALID_SCHEMES).optional().catch(undefined),
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


const validateSearch = z.object({
period: z.enum(["monthly", "yearly"]).default("monthly"),
period: z.enum(["monthly", "yearly"]).catch("monthly"),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🚩 .catch("monthly") silently normalizes ALL invalid period values, including yearly/

The .catch("monthly") on line 14 will silently convert any invalid period value to "monthly", including values like period=yearly/ where the user clearly intended yearly. This is a behavioral tradeoff worth noting: a user who intended yearly billing but whose browser appended a trailing slash will silently be redirected to a monthly checkout session instead. An alternative approach would be to strip trailing slashes before validation (e.g., via .transform(v => v.replace(/\/$/, "")) before the enum check), which would preserve the user's actual intent. The PR description flags this as a review item, and the team should decide whether silent fallback is acceptable or if normalization is preferred.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const validateSearch = z.object({
period: z.enum(["monthly", "yearly"]).default("monthly"),
period: z.enum(["monthly", "yearly"]).catch("monthly"),
scheme: z.enum(VALID_SCHEMES).optional(),
Copy link
Contributor

Choose a reason for hiding this comment

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

The scheme field has the same trailing-slash vulnerability mentioned in the PR description but wasn't fixed. If Safari URL normalization adds a trailing slash to the scheme parameter (e.g., scheme=hyprnote/), .optional() will throw a validation error instead of gracefully handling it.

Fix: Apply the same pattern:

scheme: z.enum(VALID_SCHEMES).catch(undefined).optional(),

Or alternatively:

scheme: z.enum(VALID_SCHEMES).optional().catch(undefined),

This ensures invalid scheme values fall back to undefined instead of throwing, preventing the same Sentry errors that affected the period field.

Suggested change
scheme: z.enum(VALID_SCHEMES).optional(),
scheme: z.enum(VALID_SCHEMES).catch(undefined).optional(),

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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