My Cloudflare Pages contact form could not send email, and nothing on the page ever said so
I run a small static site on Cloudflare Pages. Its contact page had a plain HTML form that posted to a Pages Function. This week I checked whether that form actually worked. It did not, and nothing a visitor could see wo
I run a small static site on Cloudflare Pages. Its contact page had a plain HTML form that posted to a Pages Function. This week I checked whether that form actually worked. It did not, and nothing a visitor could see would have told me why.
How the form was built
functions/api/contact.js reads the POST body, validates the email address and the message length, and sends the message through Brevoβs transactional email API. Whatever happens, it answers with a 303 redirect back to the contact page and carries the result in the query string: ?sent=ok, or ?sent=error&why=<reason>. The page reads that and shows a short status line.
The send step starts like this:
if (!env.BREVO_API_KEY) {
console.error('contact: BREVO_API_KEY is not set on this Pages project');
return back(url, 'error', 'config');
}
That branch is reasonable. The trouble is what happens to the error: the log line sits in Cloudflareβs function logs, and the visitor sees βThe form is not finished being set up.β
How I found out
I sent one request with a valid email address and a short message. If the key had existed, that would have delivered a single test email to my own inbox, which I was fine with.
curl -s -o /dev/null -D - -X POST https://360playzone.com/api/contact \
--data-urlencode "name=Form check" \
--data-urlencode "[email protected]" \
--data-urlencode "message=Automated form check, please ignore this message." \
--data-urlencode "website="
The empty website field is the formβs honeypot. The response headers:
HTTP/1.1 303 See Other
Location: https://360playzone.com/page/contact?sent=error&why=config#contact-form
Then I asked Cloudflare which secrets the project has:
npx wrangler pages secret list --project-name <project>
It printed the header line for the production environment and then nothing: no secrets at all. I also looked up the DNS records Brevo asks you to add for a sending domain (brevo1._domainkey, brevo2._domainkey and a DMARC record). None of them existed. So even with a key, mail from the formβs sender address would not have been authenticated.
Why nothing flagged it
- Redirect-after-POST makes the failure path and the success path look the same: both are a
303. A check that only asks whether the endpoint answered without a 5xx calls it healthy. - The error text is written for visitors, not for the person who runs the site.
- A secret that was never added to the production environment looks exactly like a working setup from the outside. I do not know how many visitors hit the error.
What I changed
A form that fails is worse than no form: a visitor types a message, presses send and gets an apology. I turned the form off behind a constant, put the email address on the page as a mailto: link, and rewrote the privacy copy that had described βthe optional contact formβ. The code stays in the repo. Turning it back on means flipping one flag once the key and the DNS records exist.
What I would do next time
- After deploying anything that reads an environment variable, send one request that exercises it and read the status or the
Locationheader. - Make the config branch loud: return a
503, or raise an alert, instead of a friendly redirect. - Add a scheduled check that posts a dummy message and expects
sent=ok. - Keep a list of the secrets each function needs next to the function, and compare it with the projectβs real list.
The site is 360 PlayZone, a free browser-games site I run. Its contact page is email-only until the sending setup is finished.
Written with AI assistance (Claude). Every measurement above comes from commands run against the live site and the Pages project.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.