-
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
revert: Don't use AMQP for emails (#482)
* revert amql * fix
- Loading branch information
Showing
4 changed files
with
199 additions
and
228 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import { v4 } from "uuid"; | ||
import axios, { AxiosError } from "axios"; | ||
import { supabaseAdmin } from "@/supabase/supabaseAdmin"; | ||
import { CheckEmailInput, CheckEmailOutput } from "@reacherhq/api"; | ||
import { Tables } from "@/supabase/database.types"; | ||
import { convertPgError } from "@/util/helpers"; | ||
|
||
// Vercel functions time out after 60s. | ||
const VERCEL_TIMEOUT = 90_000; // ms | ||
|
||
interface ReacherBackend { | ||
url: string; | ||
name: string; | ||
} | ||
|
||
const RCH_BACKENDS = [ | ||
{ | ||
url: "http://backend1.reacher.dev", | ||
name: "backend1-ovh", | ||
}, | ||
{ | ||
url: "http://backend3.reacher.dev", | ||
name: "backend3-do", | ||
}, | ||
]; | ||
|
||
/** | ||
* Forwards the Next.JS request to Reacher's backends, try them all in the | ||
* order given by `RCH_BACKENDS`. | ||
*/ | ||
export async function tryAllBackends( | ||
emailInput: CheckEmailInput, | ||
user: Tables<"users"> | ||
): Promise<Response> { | ||
try { | ||
// The final result to return. | ||
let result: CheckEmailOutput; | ||
|
||
let t: NodeJS.Timeout | undefined; | ||
|
||
async function makeBackendCalls(): Promise<Response> { | ||
// Create a unique UUID for each verification. The purpose of this | ||
// verificationId is that we insert one row in the `calls` table per | ||
// backend call. However, if we try the backends sequentially, we don't | ||
// want to charge the user 2 credits for 1 email verification. | ||
const verificationId = v4(); | ||
|
||
// Note that we don't loop the last element of reacherBackends. That | ||
// one gets treated specially, as we'll always return its response. | ||
for (let i = 0; i < RCH_BACKENDS.length - 1; i++) { | ||
try { | ||
result = await makeSingleBackendCall( | ||
verificationId, | ||
RCH_BACKENDS[i], | ||
emailInput, | ||
user | ||
); | ||
|
||
if (result.is_reachable !== "unknown") { | ||
return Response.json(result); | ||
} | ||
} catch { | ||
// Continue loop | ||
} | ||
} | ||
|
||
// If we arrive here, it means all previous backend calls errored or | ||
// returned "unknown". We make the last backend call, and always return | ||
// its response. | ||
result = await makeSingleBackendCall( | ||
verificationId, | ||
RCH_BACKENDS[RCH_BACKENDS.length - 1], | ||
emailInput, | ||
user | ||
); | ||
|
||
return Response.json(result); | ||
} | ||
|
||
const res = await Promise.race([ | ||
makeBackendCalls(), | ||
new Promise<Response>((resolve) => { | ||
t = setTimeout(() => { | ||
return resolve( | ||
Response.json( | ||
{ | ||
error: `The email ${emailInput.to_email} can't be verified within 90s. This is because the email provider imposes obstacles to prevent real-time email verification, such as greylisting. Please try again later.`, | ||
}, | ||
{ status: 504 } | ||
) | ||
); | ||
}, VERCEL_TIMEOUT); | ||
}), | ||
]); | ||
|
||
if (t) { | ||
clearTimeout(t); | ||
} | ||
|
||
return res; | ||
} catch (err) { | ||
const statusCode = (err as AxiosError).response?.status; | ||
if (!statusCode) { | ||
throw err; | ||
} | ||
|
||
return Response.json( | ||
{ | ||
error: (err as AxiosError).response?.data, | ||
}, | ||
{ | ||
status: statusCode, | ||
} | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Make a single call to the backend, and log some metadata to the DB. | ||
*/ | ||
async function makeSingleBackendCall( | ||
verificationId: string, | ||
reacherBackend: ReacherBackend, | ||
emailInput: CheckEmailInput, | ||
user: Tables<"users"> | ||
): Promise<CheckEmailOutput> { | ||
const t0 = performance.now(); | ||
// Send an API request to Reacher backend, which handles email | ||
// verifications, see https://github.com/reacherhq/backend. | ||
const result = await axios.post<CheckEmailOutput>( | ||
`${reacherBackend.url}/v0/check_email`, | ||
emailInput, | ||
{ | ||
headers: { | ||
"x-reacher-secret": process.env.RCH_HEADER_SECRET || "", | ||
}, | ||
} | ||
); | ||
const t1 = performance.now(); | ||
console.log(`[🐢] Call ${reacherBackend.name}: +${Math.round(t1 - t0)}ms`); | ||
|
||
// Get the domain of the email, i.e. the part after '@'. | ||
|
||
const parts = emailInput.to_email.split("@"); | ||
const domain = parts && parts[1]; | ||
|
||
// If successful, also log an API call entry in the database. | ||
const { error } = await supabaseAdmin.from("calls").insert({ | ||
endpoint: "/v0/check_email", | ||
user_id: user.id, | ||
backend: reacherBackend.name, | ||
backend_ip: result.request?.socket?.remoteAddress as string, | ||
domain, | ||
verification_id: verificationId, | ||
duration: | ||
(result?.data.debug?.duration.secs || 0) * 1000 + | ||
Math.round((result.data.debug?.duration.nanos || 0) * 1e-6), // in ms | ||
is_reachable: result.data.is_reachable, | ||
verif_method: result.data.debug?.smtp?.verif_method?.type, | ||
result: result.data, | ||
}); | ||
if (error) { | ||
throw convertPgError(error); | ||
} | ||
console.log(`[🐢] Log DB entry: +${Math.round(performance.now() - t1)}ms`); | ||
|
||
return result.data; | ||
} |
Oops, something went wrong.