Error when sending email takes too long

This commit is contained in:
Konstantin Wohlwend 2025-01-13 10:36:40 -08:00
parent f15de038fe
commit b7d4277510

View File

@ -6,8 +6,9 @@ import { EMAIL_TEMPLATES_METADATA, renderEmailTemplate } from '@stackframe/stack
import { ProjectsCrud } from '@stackframe/stack-shared/dist/interface/crud/projects';
import { UsersCrud } from '@stackframe/stack-shared/dist/interface/crud/users';
import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env';
import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors';
import { filterUndefined } from '@stackframe/stack-shared/dist/utils/objects';
import { StackAssertionError, captureError } from '@stackframe/stack-shared/dist/utils/errors';
import { filterUndefined, pick } from '@stackframe/stack-shared/dist/utils/objects';
import { runAsynchronously, wait } from '@stackframe/stack-shared/dist/utils/promises';
import { Result } from '@stackframe/stack-shared/dist/utils/results';
import { typedToUppercase } from '@stackframe/stack-shared/dist/utils/strings';
import nodemailer from 'nodemailer';
@ -75,113 +76,130 @@ export async function sendEmailWithKnownErrorTypes(options: SendEmailOptions): P
canRetry: boolean,
message?: string,
}>> {
return await traceSpan('sending email to ' + JSON.stringify(options.to), async () => {
try {
const transporter = nodemailer.createTransport({
host: options.emailConfig.host,
port: options.emailConfig.port,
secure: options.emailConfig.secure,
auth: {
user: options.emailConfig.username,
pass: options.emailConfig.password,
},
});
let finished = false;
runAsynchronously(async () => {
await wait(5000);
if (!finished) {
captureError("email-send-timeout", new StackAssertionError("Email send took longer than 8s; maybe the email service is too slow?", {
config: options.emailConfig.type === 'shared' ? "shared" : pick(options.emailConfig, ['host', 'port', 'username', 'senderEmail', 'senderName']),
to: options.to,
subject: options.subject,
html: options.html,
text: options.text,
}));
}
});
try {
return await traceSpan('sending email to ' + JSON.stringify(options.to), async () => {
try {
const transporter = nodemailer.createTransport({
host: options.emailConfig.host,
port: options.emailConfig.port,
secure: options.emailConfig.secure,
auth: {
user: options.emailConfig.username,
pass: options.emailConfig.password,
},
});
await transporter.sendMail({
from: `"${options.emailConfig.senderName}" <${options.emailConfig.senderEmail}>`,
...options,
});
await transporter.sendMail({
from: `"${options.emailConfig.senderName}" <${options.emailConfig.senderEmail}>`,
...options,
});
return Result.ok(undefined);
} catch (error) {
if (error instanceof Error) {
const code = (error as any).code as string | undefined;
const responseCode = (error as any).responseCode as number | undefined;
const errorNumber = (error as any).errno as number | undefined;
return Result.ok(undefined);
} catch (error) {
if (error instanceof Error) {
const code = (error as any).code as string | undefined;
const responseCode = (error as any).responseCode as number | undefined;
const errorNumber = (error as any).errno as number | undefined;
const getServerResponse = (error: any) => {
if (error?.response) {
return `\nResponse from the email server:\n${error.response}`;
const getServerResponse = (error: any) => {
if (error?.response) {
return `\nResponse from the email server:\n${error.response}`;
}
return '';
};
if (errorNumber === -3008 || code === 'EDNS') {
return Result.error({
rawError: error,
errorType: 'HOST_NOT_FOUND',
canRetry: false,
message: 'Failed to connect to the email host. Please make sure the email host configuration is correct.'
} as const);
}
return '';
};
if (errorNumber === -3008 || code === 'EDNS') {
return Result.error({
rawError: error,
errorType: 'HOST_NOT_FOUND',
canRetry: false,
message: 'Failed to connect to the email host. Please make sure the email host configuration is correct.'
} as const);
if (responseCode === 535 || code === 'EAUTH') {
return Result.error({
rawError: error,
errorType: 'AUTH_FAILED',
canRetry: false,
message: 'Failed to authenticate with the email server. Please check your email credentials configuration.',
} as const);
}
if (responseCode === 450) {
return Result.error({
rawError: error,
errorType: 'TEMPORARY',
canRetry: true,
message: 'The email server returned a temporary error. This could be due to a temporary network issue or a temporary block on the email server. Please try again later.\n\nError: ' + getServerResponse(error),
} as const);
}
if (responseCode === 553) {
return Result.error({
rawError: error,
errorType: 'INVALID_EMAIL_ADDRESS',
canRetry: false,
message: 'The email address provided is invalid. Please verify both the recipient and sender email addresses configuration are correct.\n\nError:' + getServerResponse(error),
} as const);
}
if (error.message.includes('Unexpected socket close')) {
return Result.error({
rawError: error,
errorType: 'SOCKET_CLOSED',
canRetry: false,
message: 'Connection to email server was lost unexpectedly. This could be due to incorrect email server port configuration or a temporary network issue. Please verify your configuration and try again.',
} as const);
}
}
if (responseCode === 535 || code === 'EAUTH') {
// ============ temporary error ============
const temporaryErrorIndicators = [
"450 ",
"Client network socket disconnected before secure TLS connection was established",
"Too many requests",
...options.emailConfig.host.includes("resend") ? [
// Resend is a bit unreliable, so we'll retry even in some cases where it may send duplicate emails
"ECONNRESET",
] : [],
];
if (temporaryErrorIndicators.some(indicator => error instanceof Error && error.message.includes(indicator))) {
// this can happen occasionally (especially with certain unreliable email providers)
// so let's retry
return Result.error({
rawError: error,
errorType: 'AUTH_FAILED',
canRetry: false,
message: 'Failed to authenticate with the email server. Please check your email credentials configuration.',
} as const);
}
if (responseCode === 450) {
return Result.error({
rawError: error,
errorType: 'TEMPORARY',
errorType: 'UNKNOWN',
canRetry: true,
message: 'The email server returned a temporary error. This could be due to a temporary network issue or a temporary block on the email server. Please try again later.\n\nError: ' + getServerResponse(error),
message: 'Failed to send email, but error is possibly transient due to the internet connection. Please check your email configuration and try again later.',
} as const);
}
if (responseCode === 553) {
return Result.error({
rawError: error,
errorType: 'INVALID_EMAIL_ADDRESS',
canRetry: false,
message: 'The email address provided is invalid. Please verify both the recipient and sender email addresses configuration are correct.\n\nError:' + getServerResponse(error),
} as const);
}
if (error.message.includes('Unexpected socket close')) {
return Result.error({
rawError: error,
errorType: 'SOCKET_CLOSED',
canRetry: false,
message: 'Connection to email server was lost unexpectedly. This could be due to incorrect email server port configuration or a temporary network issue. Please verify your configuration and try again.',
} as const);
}
}
// ============ temporary error ============
const temporaryErrorIndicators = [
"450 ",
"Client network socket disconnected before secure TLS connection was established",
"Too many requests",
...options.emailConfig.host.includes("resend") ? [
// Resend is a bit unreliable, so we'll retry even in some cases where it may send duplicate emails
"ECONNRESET",
] : [],
];
if (temporaryErrorIndicators.some(indicator => error instanceof Error && error.message.includes(indicator))) {
// this can happen occasionally (especially with certain unreliable email providers)
// so let's retry
// ============ unknown error ============
return Result.error({
rawError: error,
errorType: 'UNKNOWN',
canRetry: true,
message: 'Failed to send email, but error is possibly transient due to the internet connection. Please check your email configuration and try again later.',
canRetry: false,
message: 'An unknown error occurred while sending the email.',
} as const);
}
// ============ unknown error ============
return Result.error({
rawError: error,
errorType: 'UNKNOWN',
canRetry: false,
message: 'An unknown error occurred while sending the email.',
} as const);
}
});
});
} finally {
finished = true;
}
}
export async function sendEmail(options: SendEmailOptions) {