Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Dec 10, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Unified cron job error handling for consistent responses and improved reliability.
    • Added automatic retry enqueueing for transient database errors to reduce failed requests.
  • New Features

    • Introduced a cron-specific error responder and a retry-queue utility for failed API requests.
  • Refactor

    • Standardized error-response shapes across API routes and updated error handling surface.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Contributor

vercel bot commented Dec 10, 2025

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

Project Deployment Review Updated (UTC)
dub Ready Ready Preview Dec 15, 2025 4:53pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

Walkthrough

Swaps many cron routes to a new cron-specific error responder, changes the core API error helper to accept an options object (and detect transient Prisma errors), and adds a QStash retry-queue utility for retrying transient request failures.

Changes

Cohort / File(s) Summary
Cron utilities
apps/web/app/(ee)/api/cron/utils.ts
Added handleCronErrorResponse({ error }) which maps errors via handleApiError and returns a structured JSON NextResponse with appropriate status.
Cron routes β€” bulk swap
apps/web/app/(ee)/api/cron/**/route.ts (many files, e.g., aggregate-clicks, bounties/*, campaigns/broadcast, cleanup/*, commissions/export, discount-codes/*, domains/*, email-domains/*, folders/delete, framer/*, fraud/summary, fx-rates, groups/*, import/*, invoices/retry-failed, links/*, merge-partner-accounts, messages/*, network/*, online-presence/youtube, partners/*, payouts/*, program-application-reminder, send-batch-email, shopify/order-paid, streams/*, trigger-withdrawal, usage, welcome-user, workflows/*, workspaces/delete)
Replaced imports of handleAndReturnErrorResponse with handleCronErrorResponse (and logAndRespond where retained) and updated catch blocks to return handleCronErrorResponse({ error }).
Core error handler
apps/web/lib/api/errors.ts
Refactored handleAndReturnErrorResponse to accept a single options object { error, responseHeaders, requestHeaders }; exported handleApiError; added Prisma transient-error detection and QStash callback detection; sets Upstash non-retryable header/status when appropriate.
Retry queue utility
apps/web/lib/api/queue-failed-request-for-retry.ts
New queueFailedRequestForRetry that publishes retry jobs to QStash for transient Prisma errors (10s delay, up to 5 retries), guarded by API key, endpoint whitelist, and absence of Upstash signature.
Call-site signature updates (non-cron routes & middleware)
apps/web/app/(ee)/api/hubspot/*/route.ts, apps/web/app/(ee)/api/shopify/pixel/route.ts, apps/web/app/(ee)/api/singular/webhook/route.ts, apps/web/app/(ee)/api/track/*/route.ts, various apps/web/app/api/*/route.ts, apps/web/lib/auth/*, apps/web/lib/embed/*, apps/web/lib/integrations/shopify/process-order.ts, and multiple other call sites
Updated calls to handleAndReturnErrorResponse to pass an options object (e.g., { error } or { error, responseHeaders, requestHeaders }) and adjusted some catch parameter names. workspace.ts also imports and invokes queueFailedRequestForRetry for retryable errors.
Small UI / formatting changes
apps/web/ui/**, apps/web/app.dub.co/**, apps/web/lib/api/sales/construct-reward-amount.ts
Cosmetic JSX/classname/formatting tweaks with no behavior changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing careful review:
    • apps/web/lib/api/errors.ts β€” Prisma transient error detection, Upstash header handling, and new options-object deconstruction.
    • apps/web/lib/api/queue-failed-request-for-retry.ts β€” URL construction, QStash payload, delay/retry/wrapping logic and guards to avoid retry loops.
    • Representative cron routes β€” ensure imports and catch-site replacements are consistent and no cron route was missed.
    • Middleware and auth call sites that now pass requestHeaders/responseHeaders β€” confirm the new signature is used correctly everywhere.

Possibly related PRs

Suggested reviewers

  • steven-tey
  • TWilson023

Poem

🐰
Errors wrapped neat in a carrot-shaped key,
Cron routes hop home with responses that agree,
Retries queued softly for transient DB woes,
QStash hums β€œtry again” as the log-buffer grows,
Hooray β€” the rabbit patched where the error-logic flows! πŸ₯•

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.59% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
βœ… Passed checks (2 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title accurately reflects the main change: a retry mechanism is being added for failed requests across numerous cron and API routes.
✨ 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 retry-endpoints

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.

@devkiran devkiran changed the title Add retry mechanism for failed requests (WIP) Add retry mechanism for failed requests Dec 15, 2025
@devkiran devkiran marked this pull request as ready for review December 15, 2025 16:12
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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/web/app/api/analytics/dashboard/route.ts (1)

162-171: Fix waitUntil usage; error handler change is fine

The switch to handleAndReturnErrorResponse({ error }) aligns with the function's options‑object signature. However, this line:

waitUntil(await redis.set(cacheKey, response, { ex: 60 }));

awaits the Redis write before passing it to waitUntil, so waitUntil receives the resolved value ("OK") rather than the Promise. You likely want:

waitUntil(redis.set(cacheKey, response, { ex: 60 }));

so the cache write runs as deferred work as intended.

apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts (1)

202-211: QStash URL is missing the /network segment

The route is located at api/cron/network/calculate-program-similarities, but the publishJSON call at line 203 uses /api/cron/calculate-program-similarities, which is a non-existent route. This will cause scheduled follow-up batches to fail.

Update to:

-  url: `${APP_DOMAIN_WITH_NGROK}/api/cron/calculate-program-similarities`,
+  url: `${APP_DOMAIN_WITH_NGROK}/api/cron/network/calculate-program-similarities`,
apps/web/app/(ee)/api/cron/utils.ts (1)

1-33: ⚠️ Missing retry mechanism contradicts PR objectives.

The PR is titled "Add retry mechanism for failed requests" and the AI summary claims this introduces "Prisma-based transient error detection for retry decisions" and "a QStash-aware retry queue utility for failed requests." However, handleCronErrorResponse only formats error responsesβ€”it doesn't detect transient errors, queue retries, or implement any retry logic.

If the retry mechanism exists in other files not included in this review, please clarify. Otherwise, this appears to be a straightforward error handling refactoring rather than a retry implementation.

♻️ Duplicate comments (4)
apps/web/app/(ee)/api/cron/workspaces/delete/route.ts (1)

109-109: Same QStash retry logic concern as other routes.

This route also uses verifyQstashSignature (line 19) but switches to handleCronErrorResponse. Please ensure the QStash retry mechanism is preserved in the new error handler.

apps/web/app/(ee)/api/cron/cleanup/expired-tokens/route.ts (1)

67-67: Same QStash retry logic concern as other routes.

This route uses verifyQstashSignature (line 19) but switches to handleCronErrorResponse. Ensure QStash retry logic is preserved.

apps/web/app/(ee)/api/cron/payouts/process/route.ts (1)

83-83: Same QStash retry logic concern as other routes.

This route uses verifyQstashSignature (line 31) but switches to handleCronErrorResponse. Ensure QStash retry logic is preserved.

apps/web/app/(ee)/api/cron/groups/remap-default-links/route.ts (1)

10-10: Cron utils integration is consistent with other routes

Using { handleCronErrorResponse, logAndRespond } here matches the pattern in other cron routes; same note as in import/csv about confirming whether the simpler cron error handler should influence QStash retry behavior.

Also applies to: 230-238

🧹 Nitpick comments (19)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx (1)

140-141: Recommended refactor: Remove redundant " USD" suffix.

The currencyFormatter function already formats amounts with a currency symbol (e.g., $1,234.56). Appending " USD" results in redundant currency indication (e.g., $1,234.56 USD). Consider passing the currency option explicitly if needed, or removing the manual suffix.

As per the currencyFormatter documentation: the function uses Intl.NumberFormat with the currency style and defaults to USD when no explicit currency is provided.

Example refactor:

- {currencyFormatter(eligiblePendingPayouts?.amount ?? 0, {}) +
-   " USD"}
+ {currencyFormatter(eligiblePendingPayouts?.amount ?? 0, { currency: "USD" })}

You may defer this cleanup to a future PR if prioritizing other changes.

Also applies to: 198-198

apps/web/lib/auth/partner.ts (1)

171-174: Consider passing requestHeaders for QStash retry support.

The error handling update is correct and properly passes responseHeaders. However, since this function has access to req, consider also passing requestHeaders to enable QStash-aware retry logic when partner routes are called via QStash callbacks.

Apply this diff to add requestHeaders support:

       } catch (error) {
         return handleAndReturnErrorResponse({
           error,
           responseHeaders,
+          requestHeaders: req.headers,
         });
       }
apps/web/ui/modals/domain-auto-renewal-modal.tsx (1)

96-98: Minor formatting change unrelated to PR objective.

The text reflow here appears to be an unrelated formatting change. While it has no functional impact, it's worth noting that this cosmetic change doesn't align with the PR's retry mechanism objective.

apps/web/app/api/oauth/userinfo/route.ts (1)

73-77: Good improvement with consistent error naming.

The change correctly updates error handling to use the new object-based signature and improves consistency by renaming e to error. The CORS headers are properly passed as responseHeaders.

For completeness with the retry mechanism, consider also passing requestHeaders:

   } catch (error) {
     return handleAndReturnErrorResponse({
       error,
       responseHeaders: CORS_HEADERS,
+      requestHeaders: req.headers,
     });
   }
apps/web/ui/customers/customer-sales-table.tsx (1)

61-61: Cosmetic simplification - unrelated to PR objective.

The cell renderer has been simplified to a concise single-line form. While this improves readability, it's unrelated to the retry mechanism that is the focus of this PR.

apps/web/ui/customers/customer-partner-earnings-table.tsx (1)

36-36: Cosmetic simplification - unrelated to PR objective.

Both the "Sale Amount" and "Commission" cell renderers have been simplified to concise single-line forms. While this improves code consistency with customer-sales-table.tsx, these formatting changes are unrelated to the retry mechanism objective of this PR.

Also applies to: 41-41

apps/web/lib/api/queue-failed-request-for-retry.ts (2)

44-53: Consider wrapping QStash publish in try-catch.

If qstash.publishJSON fails (network issues, QStash unavailable), the error will propagate and could mask the original error or cause unexpected behavior in the caller. Since this is a best-effort retry mechanism, failures should be logged but not block the error response.

-  const response = await qstash.publishJSON({
-    url,
-    method,
-    body: await parseRequestBody(errorReq),
-    headers: {
-      ...Object.fromEntries(errorReq.headers.entries()),
-    },
-    delay: "10s",
-    retries: 5,
-  });
-
-  if (response.messageId) {
+  try {
+    const response = await qstash.publishJSON({
+      url,
+      method,
+      body: await parseRequestBody(errorReq),
+      headers: {
+        ...Object.fromEntries(errorReq.headers.entries()),
+      },
+      delay: "10s",
+      retries: 5,
+    });
+
+    if (response.messageId) {
+      console.log("Request queued for retry", {
+        method,
+        url,
+        messageId: response.messageId,
+      });
+
+      logger.info("request.retry.queued", {
+        url,
+        method,
+        messageId: response.messageId,
+      });
+
+      await logger.flush();
+    }
+  } catch (e) {
+    console.error("Failed to queue request for retry", { method, url, error: e });
+    logger.error("request.retry.queue_failed", { url, method, error: e });
+    await logger.flush();
+  }

48-50: Consider filtering sensitive headers before forwarding.

Forwarding all headers to QStash may inadvertently include sensitive information beyond the API key (e.g., cookies, internal tracing headers). Consider explicitly allowlisting headers that are necessary for retry.

+const ALLOWED_RETRY_HEADERS = [
+  "authorization",
+  "content-type",
+  "x-api-version",
+  // Add other necessary headers
+];
+
+function filterHeaders(headers: Headers): Record<string, string> {
+  const filtered: Record<string, string> = {};
+  for (const key of ALLOWED_RETRY_HEADERS) {
+    const value = headers.get(key);
+    if (value) filtered[key] = value;
+  }
+  return filtered;
+}

Then use headers: filterHeaders(errorReq.headers) instead of spreading all entries.

apps/web/lib/api/errors.ts (1)

59-63: Consider exporting ErrorHandlerOptions for caller type safety.

The interface is used in the public handleAndReturnErrorResponse signature but isn't exported. Callers may need to import this type to properly type their error handling code.

-interface ErrorHandlerOptions {
+export interface ErrorHandlerOptions {
   error: unknown;
   responseHeaders?: Headers;
   requestHeaders?: Headers;
 }
apps/web/app/(ee)/api/cron/import/csv/route.ts (1)

24-24: Confirm cron retry behavior with new error handler

This route now uses handleCronErrorResponse({ error }), which (per apps/web/app/(ee)/api/cron/utils.ts) just maps the error to { error, status } and doesn’t look at the request headers or set Upstash-NonRetryable-Error like handleAndReturnErrorResponse does. If this endpoint is invoked by QStash Cron, that may change how permanent vs transient errors are retried. Worth confirming whether cron failures should always be retried, or if you also want the non‑retryable header logic here.

Also applies to: 124-132

apps/web/app/api/links/metatags/route.ts (1)

34-45: Preserve CORS headers on error responses

Wrapping the error in { error } matches the new handleAndReturnErrorResponse signature. To keep CORS behavior consistent, consider:

} catch (error) {
  return handleAndReturnErrorResponse({
    error,
    responseHeaders: corsHeaders,
  });
}

so callers receive the same CORS headers on failures as on success.

apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts (1)

66-73: Standardized cron error response; consider tightening log typing

Switching the catch path to handleCronErrorResponse({ error }) keeps this route aligned with the new cron-wide error handling and retry semantics. To make the log call a bit safer, consider guarding error.message (as you already do in other routes) with an instanceof Error check or String(error).

apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (1)

211-218: Error response now aligned with cron-wide behavior

Delegating to handleCronErrorResponse({ error }) after logging ensures a uniform JSON+status shape for cron failures, which is important for the new retry flow. As a minor improvement, you could mirror the instanceof Error guard used in other routes to avoid relying on error.message when error might not be an Error.

apps/web/app/(ee)/api/cron/import/tolt/route.ts (1)

47-49: Consistent cron error response; consider adding a log

Returning handleCronErrorResponse({ error }) standardizes the failure shape for this import job. If you want parity with other cron routes, consider logging the error (and perhaps payload.action) before returning, to make diagnosing failed Tolt imports easier.

apps/web/app/(ee)/api/cron/groups/sync-utm/route.ts (1)

145-151: Unified cron error response; optional hardening of log

The switch to handleCronErrorResponse({ error }) after logging ensures failures from this sync job surface through the standardized cron error pipeline. As a minor improvement, you may want to mirror the instanceof Error guard pattern from other routes so error.message access is always safe.

apps/web/app/(ee)/api/cron/import/short/route.ts (1)

61-68: Cron error response integration looks good; minor logging nit

The outer catch now logs and then returns handleCronErrorResponse({ error }), which is aligned with the new cron error model and retry behavior. If you want to be defensive, you can guard error.message in the log (and in the inner catch when building the DubApiError message) in case a non-Error slips through.

apps/web/app/(ee)/api/cron/payouts/balance-available/route.ts (1)

166-173: Standardized cron error handling; optional log robustness

Using handleCronErrorResponse({ error }) after logging ensures payout failures are reported through the same cron error pipeline that drives retries and monitoring. As a small hardening step, you might guard error.message in the log with an instanceof Error check, to be resilient to non-Error throws.

apps/web/app/(ee)/api/cron/fx-rates/route.ts (1)

5-5: Cron error handler integration is correct; consider minor type-safety tweak

The import path and handleCronErrorResponse({ error }) usage look correct and align this route with the new cron error handling.

If your TS config uses useUnknownInCatchVariables, you may eventually want to narrow error before interpolating error.message in the log (e.g., error instanceof Error ? error.message : String(error)), but that’s an optional clean-up and not introduced by this PR.

Also applies to: 52-52

apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts (1)

9-9: Error routing wired correctly; log message could be clearer

The import path and handleCronErrorResponse({ error }) usage look correct and hook this updates worker into the shared cron error / retry handling.

Optionally, you might later tweak the log message (Error sending Stripe payout) to more clearly reference the payouts/process/updates job so logs are easier to grep by route, but that’s not blocking.

Also applies to: 180-180

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 (1)
apps/web/app/(ee)/api/cron/import/rebrandly/route.ts (1)

87-94: Cron error handling correctly delegates to handleCronErrorResponse (optional hardening)

Routing the outer catch through handleCronErrorResponse({ error }) standardizes cron responses and aligns with the cron-specific error pattern described in the learnings (no QStash callback detection or Upstash non‑retry headers in cron routes). Based on learnings, this is the intended behavior.

If your tsconfig uses useUnknownInCatchVariables, you might optionally normalize the error before accessing .message:

-  } catch (error) {
-    await log({
-      message: `Error importing Rebrandly links: ${error.message}`,
-      type: "cron",
-    });
-
-    return handleCronErrorResponse({ error });
-  }
+  } catch (error) {
+    const err = error instanceof Error ? error : new Error(String(error));
+
+    await log({
+      message: `Error importing Rebrandly links: ${err.message}`,
+      type: "cron",
+    });
+
+    return handleCronErrorResponse({ error: err });
+  }

Purely a safety/clarity tweak; behavior is otherwise sound.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 6711ecb and 690b960.

πŸ“’ Files selected for processing (2)
  • apps/web/app/(ee)/api/cron/cleanup/demo-embed-partners/route.ts (2 hunks)
  • apps/web/app/(ee)/api/cron/import/rebrandly/route.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/(ee)/api/cron/cleanup/demo-embed-partners/route.ts
🧰 Additional context used
🧠 Learnings (5)
πŸ““ Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 3213
File: apps/web/app/(ee)/api/cron/auto-approve-partner/route.ts:122-122
Timestamp: 2025-12-15T16:45:51.667Z
Learning: In the Dub codebase, cron endpoints under apps/web/app/(ee)/api/cron/ use handleCronErrorResponse for error handling, which intentionally does NOT detect QStash callbacks or set Upstash-NonRetryable-Error headers. This allows QStash to retry all cron job errors using its native retry mechanism. The selective retry logic (queueFailedRequestForRetry) is only used for specific user-facing API endpoints like /api/track/lead, /api/track/sale, and /api/links to retry only transient Prisma database errors.
πŸ“š Learning: 2025-12-09T12:54:41.818Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3207
File: apps/web/lib/cron/with-cron.ts:27-56
Timestamp: 2025-12-09T12:54:41.818Z
Learning: In `apps/web/lib/cron/with-cron.ts`, the `withCron` wrapper extracts the request body once and provides it to handlers via the `rawBody` parameter. Handlers should use this `rawBody` string parameter (e.g., `JSON.parse(rawBody)`) rather than reading from the Request object via `req.json()` or `req.text()`.

Applied to files:

  • apps/web/app/(ee)/api/cron/import/rebrandly/route.ts
πŸ“š Learning: 2025-12-15T16:45:51.667Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3213
File: apps/web/app/(ee)/api/cron/auto-approve-partner/route.ts:122-122
Timestamp: 2025-12-15T16:45:51.667Z
Learning: In cron endpoints under apps/web/app/(ee)/api/cron, continue using handleCronErrorResponse for error handling. Do not detect QStash callbacks or set Upstash-NonRetryable-Error headers in these cron routes, so QStash can retry cron errors via its native retry mechanism. The existing queueFailedRequestForRetry logic should remain limited to specific user-facing API endpoints (e.g., /api/track/lead, /api/track/sale, /api/links) to retry only transient Prisma/database errors. This pattern should apply to all cron endpoints under the cron directory in this codebase.

Applied to files:

  • apps/web/app/(ee)/api/cron/import/rebrandly/route.ts
πŸ“š Learning: 2025-05-29T09:49:19.604Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2433
File: apps/web/ui/modals/add-payment-method-modal.tsx:60-62
Timestamp: 2025-05-29T09:49:19.604Z
Learning: The `/api/workspaces/${slug}/billing/payment-methods` POST endpoint in the billing API returns either an error (handled by response.ok check) or a response object containing a `url` property for successful requests.

Applied to files:

  • apps/web/app/(ee)/api/cron/import/rebrandly/route.ts
πŸ“š Learning: 2025-06-18T20:26:25.177Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2538
File: apps/web/ui/partners/overview/blocks/commissions-block.tsx:16-27
Timestamp: 2025-06-18T20:26:25.177Z
Learning: In the Dub codebase, components that use workspace data (workspaceId, defaultProgramId) are wrapped in `WorkspaceAuth` which ensures these values are always available, making non-null assertions safe. This is acknowledged as a common pattern in their codebase, though not ideal.

Applied to files:

  • apps/web/app/(ee)/api/cron/import/rebrandly/route.ts
🧬 Code graph analysis (1)
apps/web/app/(ee)/api/cron/import/rebrandly/route.ts (1)
apps/web/app/(ee)/api/cron/utils.ts (1)
  • handleCronErrorResponse (22-33)
⏰ 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: build
πŸ”‡ Additional comments (2)
apps/web/app/(ee)/api/cron/import/rebrandly/route.ts (2)

1-7: Error-handling imports are consistent and necessary

Keeping DubApiError for explicit bad_request throws and adding handleCronErrorResponse for the top-level catch cleanly separates domain errors from cron response formatting. This matches the shared cron-utils pattern; no issues here.


82-85: Workspace error message formatting fix looks good

The updated message string:

message: `Workspace: ${workspace?.slug || workspaceId}. Error: ${error.message}`,

removes the stray $ and reads clearly while preserving the workspace context and underlying error. All good here.

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.

2 participants