Skip to content

Commit

Permalink
fix: Better error messages
Browse files Browse the repository at this point in the history
  • Loading branch information
amaury1093 committed Dec 11, 2023
1 parent 05849a7 commit 3cedb74
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 66 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@stripe/stripe-js": "^1.52.0",
"@supabase/supabase-js": "^1.35.7",
"@types/amqplib": "^0.10.4",
"@types/async-retry": "^1.4.8",
"@types/cors": "^2.8.17",
"@types/mailgun-js": "^0.22.18",
"@types/markdown-pdf": "^9.0.5",
Expand All @@ -32,6 +33,7 @@
"@types/request-ip": "^0.0.41",
"@types/uuid": "^9.0.7",
"amqplib": "^0.10.3",
"async-retry": "^1.3.3",
"axios": "^1.6.2",
"cors": "^2.8.5",
"date-fns": "^2.30.0",
Expand Down
16 changes: 15 additions & 1 deletion src/components/Demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { useUser } from "@/util/useUser";

function alertError(email: string, e: string) {
alert(
`An unexpected error happened. Can you email [email protected] with this message (or a screenshot)?\n\nEmail: ${email}\n${e}`
`An unexpected error happened. Can you email [email protected] with this message (or a screenshot)?
Email: ${email}
Error: ${e}`
);
}

Expand Down Expand Up @@ -39,6 +42,7 @@ export function Demo({ onVerified }: DemoProps): React.ReactElement {
}

setLoading(true);
console.log("[/dashboard] Verifying email", email);
postData<CheckEmailOutput>({
url: `/api/v0/check_email`,
token: userDetails?.api_token,
Expand All @@ -51,6 +55,16 @@ export function Demo({ onVerified }: DemoProps): React.ReactElement {
setLoading(false);
return onVerified && onVerified(r);
})
.catch((err: Error) => {
if (err.message.includes("The result contains 0 rows")) {
throw new Error(
`The email ${email} can't be verified within 1 minute. This is because the email provider imposes obstacles to prevent real-time email verification, such as greylisting.
Please try again later.`
);
}

throw err;
})
.catch((err: Error) => {
sentryException(err);
alertError(email, err.message);
Expand Down
69 changes: 43 additions & 26 deletions src/pages/api/v0/check_email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { SupabaseCall } from "@/util/supabaseClient";
import { supabaseAdmin } from "@/util/supabaseServer";
import { WebhookExtra } from "../calls/webhook";

const TIMEOUT = 60000;
const TIMEOUT = 50000;
const MAX_PRIORITY = 5; // Higher is faster, 5 is max.

const POST = async (
Expand All @@ -37,10 +37,23 @@ const POST = async (
try {
const verificationId = v4();

const conn = await amqplib.connect(
process.env.RCH_AMQP_ADDR || "amqp://localhost"
);
const ch1 = await conn.createChannel();
const conn = await amqplib
.connect(process.env.RCH_AMQP_ADDR || "amqp://localhost")
.catch((err) => {
const message = `Error connecting to RabbitMQ: ${
(err as AggregateError).errors
? (err as AggregateError).errors
.map((e) => e.message)
.join(", ")
: err.message
}`;

throw new Error(message);
});

const ch1 = await conn.createChannel().catch((err) => {
throw new Error(`Error creating RabbitMQ channel: ${err.message}`);
});
const verifMethod = await getVerifMethod(req.body as CheckEmailInput);
const queueName = `check_email.${
// If the verifMethod is "Api", we use the "Headless" queue instead,
Expand Down Expand Up @@ -135,28 +148,32 @@ export default POST;
// getVerifMethod returns the verifMethod that is best used to verify the
// input's email address.
async function getVerifMethod(input: CheckEmailInput): Promise<string> {
const domain = input.to_email.split("@")[1];
if (!domain) {
return "Smtp";
}
try {
const domain = input.to_email.split("@")[1];
if (!domain) {
return "Smtp";
}

const records = await dns.resolveMx(domain);
if (
input.yahoo_verif_method !== "Smtp" &&
records.some((r) => r.exchange.endsWith(".yahoodns.net")) // Note: there's no "." at the end of the domain.
) {
return "Headless";
} else if (
input.hotmail_verif_method !== "Smtp" &&
records.some((r) => r.exchange.endsWith(".protection.outlook.com")) // Note: there's no "." at the end of the domain.
) {
return "Headless";
} else if (
input.gmail_verif_method !== "Smtp" &&
records.some((r) => r.exchange.endsWith(".google.com")) // Note: there's no "." at the end of the domain.
) {
return "Api";
} else {
const records = await dns.resolveMx(domain);
if (
input.yahoo_verif_method !== "Smtp" &&
records.some((r) => r.exchange.endsWith(".yahoodns.net")) // Note: there's no "." at the end of the domain.
) {
return "Headless";
} else if (
input.hotmail_verif_method !== "Smtp" &&
records.some((r) => r.exchange.endsWith(".protection.outlook.com")) // Note: there's no "." at the end of the domain.
) {
return "Headless";
} else if (
input.gmail_verif_method !== "Smtp" &&
records.some((r) => r.exchange.endsWith(".google.com")) // Note: there's no "." at the end of the domain.
) {
return "Api";
} else {
return "Smtp";
}
} catch (err) {
return "Smtp";
}
}
30 changes: 29 additions & 1 deletion src/util/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { addMonths, differenceInMilliseconds, parseISO } from "date-fns";
import { NextApiRequest, NextApiResponse } from "next";
import { RateLimiterRes } from "rate-limiter-flexible";

import { setRateLimitHeaders } from "./helpers";
import { subApiMaxCalls } from "./subs";
import { SupabaseUser } from "./supabaseClient";
import {
Expand Down Expand Up @@ -112,3 +111,32 @@ export async function checkUserInDB(

return { user, sentResponse: false };
}

/**
* Sets the Rate Limit headers on the response.
*
* @param res - The NextJS API response.
* @param rateLimiterRes - The response object from rate-limiter-flexible.
* @param limit - The limit per interval.
*/
function setRateLimitHeaders(
res: NextApiResponse,
rateLimiterRes: RateLimiterRes,
limit: number
): void {
const headers = {
"Retry-After": rateLimiterRes.msBeforeNext / 1000,
"X-RateLimit-Limit": limit,
// When I first introduced rate limiting, some users had used for than
// 10k emails per month. Their remaining showed e.g. -8270. We decide
// to show 0 in these cases, hence the Math.max.
"X-RateLimit-Remaining": Math.max(0, rateLimiterRes.remainingPoints),
"X-RateLimit-Reset": new Date(
Date.now() + rateLimiterRes.msBeforeNext
).toISOString(),
};

Object.keys(headers).forEach((k) =>
res.setHeader(k, headers[k as keyof typeof headers])
);
}
60 changes: 22 additions & 38 deletions src/util/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios, { AxiosError, AxiosResponse } from "axios";
import { NextApiResponse } from "next";
import { RateLimiterRes } from "rate-limiter-flexible";
import retry from "async-retry";

// Gets the currently depoloyed URL.
export const getWebappURL = (): string => {
Expand Down Expand Up @@ -28,21 +29,33 @@ export const postData = async <T = unknown>({
data?: unknown;
}): Promise<T> => {
try {
const { data: res } = await axios.post<T, AxiosResponse<T>>(url, data, {
headers: {
"Content-Type": "application/json",
token,
Authorization: token,
},
withCredentials: true,
});
return await retry(
async () => {
const { data: res } = await axios.post<T, AxiosResponse<T>>(
url,
data,
{
headers: {
"Content-Type": "application/json",
token,
Authorization: token,
},
withCredentials: true,
}
);

return res;
return res;
},
{
retries: 2,
}
);
} catch (err) {
throw convertAxiosError(err as AxiosError);
}
};

// Converts an AxiosError to a regular Error with nice formatting.
export function convertAxiosError(err: AxiosError): Error {
if (err instanceof AxiosError) {
// Inspired by https://stackoverflow.com/questions/49967779/axios-handling-errors
Expand Down Expand Up @@ -87,32 +100,3 @@ export function parseHashComponents(hash: string): Record<string, string> {
return acc;
}, {} as Record<string, string>);
}

/**
* Sets the Rate Limit headers on the response.
*
* @param res - The NextJS API response.
* @param rateLimiterRes - The response object from rate-limiter-flexible.
* @param limit - The limit per interval.
*/
export function setRateLimitHeaders(
res: NextApiResponse,
rateLimiterRes: RateLimiterRes,
limit: number
): void {
const headers = {
"Retry-After": rateLimiterRes.msBeforeNext / 1000,
"X-RateLimit-Limit": limit,
// When I first introduced rate limiting, some users had used for than
// 10k emails per month. Their remaining showed e.g. -8270. We decide
// to show 0 in these cases, hence the Math.max.
"X-RateLimit-Remaining": Math.max(0, rateLimiterRes.remainingPoints),
"X-RateLimit-Reset": new Date(
Date.now() + rateLimiterRes.msBeforeNext
).toISOString(),
};

Object.keys(headers).forEach((k) =>
res.setHeader(k, headers[k as keyof typeof headers])
);
}
24 changes: 24 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1960,6 +1960,13 @@
dependencies:
"@types/node" "*"

"@types/async-retry@^1.4.8":
version "1.4.8"
resolved "https://registry.yarnpkg.com/@types/async-retry/-/async-retry-1.4.8.tgz#eb32df13aceb9ba1a8a80e7fe518ff4e3fe46bb3"
integrity sha512-Qup/B5PWLe86yI5I3av6ePGaeQrIHNKCwbsQotD6aHQ6YkHsMUxVZkZsmx/Ry3VZQ6uysHwTjQ7666+k6UjVJA==
dependencies:
"@types/retry" "*"

"@types/async@^3.2.16":
version "3.2.18"
resolved "https://registry.yarnpkg.com/@types/async/-/async-3.2.18.tgz#3d93dde6eab654f7bc23e549d9af5d3fa4a5bdc5"
Expand Down Expand Up @@ -2178,6 +2185,11 @@
dependencies:
"@types/node" "*"

"@types/retry@*":
version "0.12.5"
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.5.tgz#f090ff4bd8d2e5b940ff270ab39fd5ca1834a07e"
integrity sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==

"@types/scheduler@*":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
Expand Down Expand Up @@ -2729,6 +2741,13 @@ astring@^1.6.0:
resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.4.tgz#6d4c5d8de7be2ead9e4a3cc0e2efb8d759378904"
integrity sha512-97a+l2LBU3Op3bBQEff79i/E4jMD2ZLFD8rHx9B6mXyB2uQwhJQYfiDqUwtfjF4QA1F2qs//N6Cw8LetMbQjcw==

async-retry@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280"
integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==
dependencies:
retry "0.13.1"

async-sema@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/async-sema/-/async-sema-3.1.1.tgz#e527c08758a0f8f6f9f15f799a173ff3c40ea808"
Expand Down Expand Up @@ -7682,6 +7701,11 @@ restore-cursor@^3.1.0:
onetime "^5.1.0"
signal-exit "^3.0.2"

[email protected]:
version "0.13.1"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==

reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
Expand Down

1 comment on commit 3cedb74

@vercel
Copy link

@vercel vercel bot commented on 3cedb74 Dec 11, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.