Skip to content

Say so when part of a search fails; send the help form via our own route (#321) - #400

Merged
adamjohnwright merged 5 commits into
mainfrom
fix/321-search-errors
Oct 10, 2026
Merged

adamjohnwright merged 5 commits into
mainfrom
fix/321-search-errors

Conversation

@adamjohnwright

@adamjohnwright adamjohnwright commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Part of #321.

Silent failures, now said

What failed Before Now
A batch of the protein list Dropped; the list came up short "Some proteins couldn't be loaded, so this list is incomplete. Please try again later."
Another page of a result group Page number moved, old entries stayed Stays on the page shown; "Couldn't load that page of results. Please try again later."
The website-pages index Replaced by an empty index for the session "Couldn't search the website's pages, so only Reactome data is shown. Please try again later." Shown with or without data results, and the next search retries the index.
Sending the help-desk message "Thank you for contacting us." Form stays with what was written; "Your message couldn't be sent. Please try again later, or email help@reactome.org."

A 404 "No entries found" is still read as no results.

The help form now uses our own route and Cloudflare Turnstile

The form posted to ContentService's /contact, which accepts only an hCaptcha token checked with a secret held there. The site's other human checks use Cloudflare Turnstile.

/contact is now a node route in serve-prod (contact-route.js):

  • Turnstile on every message: there is no switch to turn it off, since an unchecked form that sends mail is a spam relay.
  • Rate limits: per sender, plus a shared hourly total. Only verified messages count toward the shared total.
  • Mail handling: the message goes to the local MTA (sendmail -t -i), with a timeout. Recipients come only from settings. The reader's address appears only in Reply-To, after validation. The subject is RFC 2047 encoded and the body is quoted-printable.
  • Settings: CONTACT_TO and CONTACT_FROM. Without them, or without Turnstile's keys, GET /contact answers 503 and the form says to email help@reactome.org.
  • "Send me a copy" is removed: mailing whatever address is typed in would let the form mail anyone.

Also fixed

  • Double post: the help form posted every message twice, because (submit) and (ngSubmit) were both bound. A double click now sends once as well.
  • Send stuck disabled: if the check was solved before the email was typed, Send stayed disabled, because nothing re-checked the fields in the zoneless app.
  • Help form across searches: each search that finds nothing gets a fresh form and its own widget. Before, the captcha was drawn once, so a later form could not be sent.
  • Pages group paging: the Pages group is paged in the browser. Asking the search service for "Pages" filled it with data results.
  • Stale requests: an earlier search's requests are cancelled, so they cannot write into the next search's results.
  • Missing icon: the "Icon" result type asked for an SVG that does not exist. It is now served from general/.

Tests

e2e (e2e/search-errors.spec.ts). Each test is shown failing first. The help-form tests stand in for Cloudflare and for /contact, so nothing is ever sent.

vitest (contact-route.spec.mjs). These cover:

  • refusals, and header injection through the address and the subject;
  • encoding;
  • rate limits, including that refused tokens cannot spend the shared total;
  • a deployment with no settings;
  • the sendmail invocation, run against a stand-in script, including a hang and an exit before reading.

To deploy: set CONTACT_TO and CONTACT_FROM in the service's environment, then restart serve-prod.

🤖 Generated with Claude Code

adamjohnwright and others added 4 commits October 10, 2026 17:25
Each of these turned a failure into less, silently:
- a protein batch that failed was dropped, so the list came up short;
- a group's page number moved before its entries, so a failed page change
  labelled the old entries with the new number;
- a failed site-page index was replaced, for the rest of the session, by an
  empty one, so the website's pages were missing from every search;
- a help-desk message that failed to send was thanked for anyway.

Each now says so where its results would be. The site index is retried by the
next search.

Also, the help form:
- posted every message twice, as both (submit) and (ngSubmit) were bound;
- left Send disabled for good if the captcha was solved before the email was
  typed, since nothing re-checked the fields as they changed.

And the "Icon" result type asked for an SVG that does not exist, logging an
error on every search with icon results; it is served from general/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review:
- The pages notice now shows whatever the data search found, including
  nothing, where the pages were the half that might have had something.
- A new search resets the help form: its failure, sent state and readiness,
  and draws it a captcha of its own. Drawn once, a later form had none and
  could not be sent; a solved token carried over and enabled Send with an
  empty email. The captcha script loads once and its failure is reported.
- Pages are paged on the client, as they are built there: asking the search
  service for "Pages" put its data results under that heading.
- A group page that comes back empty keeps the number on what is shown.
- An earlier search's requests are cancelled, so they cannot write into the
  next one's results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form posted to ContentService's /contact, which takes only an hCaptcha
token checked with a secret held there; the site's other human checks are
Cloudflare Turnstile. /contact is now ours (serve-prod): it checks a Turnstile
token on every message, limits how often one address writes, and hands the
mail to the local MTA. Where it goes and whom it is from are settings,
CONTACT_TO and CONTACT_FROM; without them, or without Turnstile's keys, it says
it cannot send, and the form says to email help@reactome.org.

The form draws a Turnstile widget per form, re-arms it after a failed send,
and says so when the widget or the route is unavailable. "Send me a copy" is
gone: mailing whatever address is typed in would let the form mail anyone.

Also: an all-failed search no longer also claims "only Reactome data is shown";
a cancelled protein fetch no longer leaves the list marked loading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review of the contact route:
- A message spends the shared hourly budget only once its Turnstile token is
  accepted; a refused token spends only its sender's. Unverified calls from a
  few addresses could otherwise close the form for everyone.
- sendmail has a timeout, and an error writing to it (EPIPE) is the request's
  failure rather than an uncaught one that stops the server.
- The Turnstile check times out, and is given the reader's address.
- The body is quoted-printable, so a long paragraph is not broken at 998
  characters; a long subject is folded into encoded words of legal length.
- The signature no longer repeats the request's Host header.
- Send waits for its answer, so a double click sends once; the widget and its
  request are cleaned up with the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adamjohnwright adamjohnwright changed the title Say so when part of a search fails, and send a help message once (#321) Say so when part of a search fails; send the help form via our own route (#321) Oct 10, 2026
Checking the limit and counting the message were split around the Turnstile
check, so messages sent together all passed before any was counted. An
address's slot is now taken at once; only the shared total waits for the
check. Cloudflare is given the reader's address only when it is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adamjohnwright
adamjohnwright merged commit 7ce0289 into main Oct 10, 2026
6 checks passed
@adamjohnwright
adamjohnwright deleted the fix/321-search-errors branch October 10, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant