+
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export async function POST(req: Request) {
console.log(
`Sending emails to ${programEnrollmentChunk.length} partners: ${programEnrollmentChunk.map(({ partner }) => partner.email).join(", ")}`,
);
await resend?.batch.send(
await resend.batch.send(
programEnrollmentChunk.map(({ partner }) => ({
from: VARIANT_TO_FROM_MAP.notifications,
to: partner.email!, // coerce the type here because we've already filtered out partners with no email in the prisma query
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export async function GET(req: Request) {
const reminderDomainsChunks = chunk(reminderDomains, 100);

for (const reminderDomainsChunk of reminderDomainsChunks) {
const res = await resend?.batch.send(
const res = await resend.batch.send(
reminderDomainsChunk.map(({ workspace, user, domain }) => ({
from: VARIANT_TO_FROM_MAP.notifications,
to: user.email!,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(ee)/api/cron/merge-partner-accounts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export async function POST(req: Request) {
// Make sure the cache is cleared
await redis.del(`${CACHE_KEY_PREFIX}:${userId}`);

await resend?.batch.send([
await resend.batch.send([
{
from: VARIANT_TO_FROM_MAP.notifications,
to: sourceEmail,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export async function sendPaypalPayouts({ invoiceId }: { invoiceId: string }) {

console.log("PayPal batch payout created", batchPayout);

const batchEmails = await resend?.batch.send(
const batchEmails = await resend.batch.send(
payouts
.filter((payout) => payout.partner.email)
.map((payout) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export async function sendStripePayouts({ invoiceId }: { invoiceId: string }) {
await new Promise((resolve) => setTimeout(resolve, 250));
}

const resendBatch = await resend?.batch.send(
const resendBatch = await resend.batch.send(
currentInvoicePayouts
.filter((p) => p.partner.email)
.map((p) => {
Expand Down
15 changes: 1 addition & 14 deletions apps/web/app/(ee)/api/cron/payouts/reminders/partners/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { resend } from "@dub/email/resend";
import { VARIANT_TO_FROM_MAP } from "@dub/email/resend/constants";
import ConnectPayoutReminder from "@dub/email/templates/connect-payout-reminder";
import { prisma } from "@dub/prisma";
import { chunk, log } from "@dub/utils";
import { chunk } from "@dub/utils";
import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";
Expand Down Expand Up @@ -65,19 +65,6 @@ export async function GET(req: Request) {
}),
]);

if (!resend) {
await log({
message: "Resend is not configured, skipping email sending.",
type: "errors",
});

console.warn("Resend is not configured, skipping email sending.");

return NextResponse.json(
"Resend is not configured, skipping email sending.",
);
}

const partnerProgramMap = new Map<
string,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ export async function GET(req: Request) {
},
});

// if there are no pending payouts, skip this program
if (!pendingPayout._sum?.amount) {
continue;
}

pendingPayouts.push({
programId: program.id,
_sum: pendingPayout._sum,
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ async function processDomainRenewalInvoice({ invoice }: { invoice: Invoice }) {
);

if (workspaceOwners.length > 0) {
await resend?.batch.send(
await resend.batch.send(
workspaceOwners.map(({ user }) => ({
from: VARIANT_TO_FROM_MAP.notifications,
to: user.email!,
Expand Down Expand Up @@ -275,7 +275,7 @@ async function processDomainRenewalInvoice({ invoice }: { invoice: Invoice }) {
});

if (workspaceOwners.length > 0) {
await resend?.batch.send(
await resend.batch.send(
workspaceOwners.map(({ user }) => ({
from: VARIANT_TO_FROM_MAP.notifications,
to: user.email!,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ async function processDomainRenewalInvoice({ invoice }: { invoice: Invoice }) {
return;
}

await resend?.batch.send(
await resend.batch.send(
workspaceOwners.map(({ user }) => ({
from: VARIANT_TO_FROM_MAP.notifications,
to: user.email!,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/actions/partners/bulk-approve-partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export const bulkApprovePartnersAction = authActionClient

await Promise.allSettled([
// Send approval emails
...emailChunks.map((emailChunk) => resend?.batch.send(emailChunk)),
...emailChunks.map((emailChunk) => resend.batch.send(emailChunk)),

// Send enrolled webhooks
...updatedEnrollments.map(({ partner, ...enrollment }) =>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/actions/partners/create-bounty-submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export const createBountySubmissionAction = authPartnerActionClient
});

if (users.length > 0) {
await resend?.batch.send(
await resend.batch.send(
users.map((user) => ({
from: VARIANT_TO_FROM_MAP.notifications,
to: user.email,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/actions/partners/merge-partner-accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ const sendTokens = async ({
],
});

await resend?.batch.send([
await resend.batch.send([
{
from: VARIANT_TO_FROM_MAP.notifications,
to: sourceEmail,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/api/domains/claim-dot-link-domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export const sendDomainClaimedEmails = async ({
}));

if (emails.length > 0) {
return await resend?.batch.send(emails);
return await resend.batch.send(emails);
}

return null;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/api/partners/notify-partner-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ export async function notifyPartnerApplication({

// Send all emails in batches
await Promise.all(
emailChunks.map((emailChunk) => resend?.batch.send(emailChunk)),
emailChunks.map((emailChunk) => resend.batch.send(emailChunk)),
);
}
2 changes: 1 addition & 1 deletion apps/web/lib/api/partners/notify-partner-commission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,6 @@ export async function notifyPartnerCommission({

// Send all emails in batches
await Promise.all(
emailChunks.map((emailChunk) => resend?.batch.send(emailChunk)),
emailChunks.map((emailChunk) => resend.batch.send(emailChunk)),
);
}
2 changes: 1 addition & 1 deletion apps/web/lib/partners/approve-partner-enrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export async function approvePartnerEnrollment({

...(partnerEmailsToNotify.length
? [
resend?.batch.send(
resend.batch.send(
partnerEmailsToNotify.map((email) => ({
subject: `Your application to join ${program.name} partner program has been approved!`,
from: VARIANT_TO_FROM_MAP.notifications,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/analytics/feedback/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export async function submitFeedback(data: FormData) {
const email = data.get("email") as string;
const feedback = data.get("feedback") as string;

return await resend?.emails.send({
return await resend.emails.send({
from: "feedback@dub.co",
to: "steven@dub.co",
...(email && { replyTo: email }),
Expand Down
3 changes: 1 addition & 2 deletions packages/email/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { resend } from "./resend";
import { ResendEmailOptions } from "./resend/types";
import { sendViaNodeMailer } from "./send-via-nodemailer";
import { sendEmailViaResend } from "./send-via-resend";

export const sendEmail = async (opts: ResendEmailOptions) => {
if (resend) {
if (process.env.RESEND_API_KEY) {
return await sendEmailViaResend(opts);
}

Expand Down
4 changes: 1 addition & 3 deletions packages/email/src/resend/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Resend } from "resend";

export const resend = process.env.RESEND_API_KEY
? new Resend(process.env.RESEND_API_KEY)
: null;
export const resend = new Resend(process.env.RESEND_API_KEY);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
export const resend = new Resend(process.env.RESEND_API_KEY);
export const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null;

The Resend client will be instantiated with undefined when RESEND_API_KEY is not set, which will likely cause runtime errors when the client is used.

View Details

Analysis

Resend Client Instantiation Bug: Runtime Error When API Key Not Set

Issue Summary

The Resend email client in packages/email/src/resend/client.ts is instantiated unconditionally with process.env.RESEND_API_KEY, which causes immediate runtime errors when the environment variable is undefined. This creates application crashes at startup in environments where the API key is not configured.

Technical Details

Current Problematic Code

// packages/email/src/resend/client.ts
export const resend = new Resend(process.env.RESEND_API_KEY);

Error Behavior

When process.env.RESEND_API_KEY is undefined, the Resend constructor immediately throws:

Error: Missing API key. Pass it to the constructor `new Resend("re_123")`

This error occurs at module import time, not when the client is used, meaning any application importing this module will crash during startup if the environment variable is not set.

Codebase Inconsistency

The codebase shows an inconsistent approach to handling the missing API key scenario:

  1. Environment variable checks: Some files properly check process.env.RESEND_API_KEY:

    // packages/email/src/index.ts
    if (process.env.RESEND_API_KEY) {
      return await sendEmailViaResend(opts);
    }
  2. Client null checks: Other files attempt to check the resend client itself:

    // packages/email/src/send-via-resend.ts
    if (!resend) {
      console.info("RESEND_API_KEY is not set in the .env. Skipping sending email.");
      return;
    }

However, the client null checks are unreachable code because the constructor throws an error before the client variable can be assigned a value.

Impact

This bug manifests as:

  • Application crashes during startup/import in development environments without API keys
  • Deployment failures in environments where Resend is not configured
  • Inconsistent error handling where some parts of the application expect graceful degradation but get hard crashes instead

Verification Evidence

Through concrete testing with Resend v4.0.0, I confirmed:

  1. new Resend(undefined) immediately throws an error
  2. Module import fails when the constructor is called with undefined
  3. The error occurs before any null checks can be executed
  4. The conditional pattern process.env.RESEND_API_KEY ? new Resend(...) : null works correctly

Recommended Fix

Use conditional instantiation to allow graceful handling of missing API keys:

export const resend = process.env.RESEND_API_KEY 
  ? new Resend(process.env.RESEND_API_KEY) 
  : null;

This approach:

  • Prevents startup crashes when API key is not configured
  • Makes the existing null checks functional
  • Maintains consistency with the codebase's defensive programming patterns
  • Allows applications to gracefully degrade or use alternative email providers

4 changes: 2 additions & 2 deletions packages/email/src/resend/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export async function subscribe({
name?: string | null;
audience?: keyof typeof RESEND_AUDIENCES;
}) {
if (!resend) {
if (!process.env.RESEND_API_KEY) {
console.error(
"Resend client is not initialized. This may be due to a missing or invalid RESEND_API_KEY in the .env file. Skipping.",
"No RESEND_API_KEY is set in the environment variables. Skipping.",
);
return;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/email/src/resend/unsubscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ export async function unsubscribe({
email: string;
audience?: keyof typeof RESEND_AUDIENCES;
}) {
if (!resend) {
if (!process.env.RESEND_API_KEY) {
console.error(
"Resend client is not properly initialized. Skipping operation.",
"No RESEND_API_KEY is set in the environment variables. Skipping.",
);
return;
}
Expand Down
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载