From bfe6469a4697ef381138dba16fd6cab3b390455b Mon Sep 17 00:00:00 2001 From: Calum Muir Date: Sun, 28 Dec 2025 13:34:18 +0000 Subject: [PATCH] Add timeouts for Turnstile and Resend --- functions/api/contact.ts | 43 ++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/functions/api/contact.ts b/functions/api/contact.ts index 4abe67a..1770042 100644 --- a/functions/api/contact.ts +++ b/functions/api/contact.ts @@ -11,6 +11,25 @@ const jsonResponse = (data: Record, status = 200) => headers: { "content-type": "application/json" }, }); +const fetchWithTimeout = async ( + input: RequestInfo | URL, + init: RequestInit, + timeoutMs: number, + label: string, +) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(input, { ...init, signal: controller.signal }); + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown error"; + throw new Error(`${label} request failed: ${message}`); + } finally { + clearTimeout(timeout); + } +}; + const readFormField = (value: FormDataEntryValue | null) => typeof value === "string" ? value.trim() : ""; const readTextField = (value: string | null) => (value ?? "").trim(); @@ -68,12 +87,15 @@ const handlePost: PagesFunction = async ({ request, env }) => { remoteip: request.headers.get("CF-Connecting-IP") ?? "", }); - const verifyResponse = await fetch( + const verifyResponse = await fetchWithTimeout( "https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, body: verifyBody, }, + 8000, + "Turnstile", ); const verifyResult = (await verifyResponse.json()) as { success?: boolean; @@ -133,14 +155,19 @@ const handlePost: PagesFunction = async ({ request, env }) => { text: `Name: ${name}\nEmail: ${email}\n\n${message}`, }; - const response = await fetch("https://api.resend.com/emails", { - method: "POST", - headers: { - Authorization: `Bearer ${env.RESEND_API_KEY}`, - "Content-Type": "application/json", + const response = await fetchWithTimeout( + "https://api.resend.com/emails", + { + method: "POST", + headers: { + Authorization: `Bearer ${env.RESEND_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(emailPayload), }, - body: JSON.stringify(emailPayload), - }); + 8000, + "Resend", + ); if (!response.ok) { const contentType = response.headers.get("content-type") ?? ""; const errorText = contentType.includes("application/json")