← All articles

We Removed a Third-Party Form Handler. It Also Removed Our Safety Net.

Last week we removed a third-party form relay from one of our own marketing sites. The reasoning was simple and, we still think, correct: when our owned form endpoint failed, the page quietly re-posted the visitor's name, email and free-text message to an external processor we had never assessed. A privacy fallback that only fires when things are already going wrong is the least likely path in your whole system to be noticed — which is exactly why it should not be carrying anyone's personal data.

So we deleted it. Tests were added to make sure it could not come back. CI was green in the front-end repository, and the visitor-facing behaviour got better: instead of a silent hand-off to a stranger, a failed submission now shows an error and offers an email address we own.

What nobody noticed is that the relay had a second job.

The error code that was a handoff

Our intake API — a separate service, in a separate repository — ends like this when neither of its durable destinations accepts a lead:

if not (stored or emailed):
    raise HTTPException(502, "Intake sink unavailable; retry via fallback.")

Directly above it, in the module's own documentation, sat this sentence: "the static form can transparently fall back to its third-party handler — no lead is ever lost."

Read those two things together and the design is sound. The 502 was never a failure message. It was a handoff: a signal to the page saying "I could not take this one, you take it." The promise that no lead is ever lost was true — but it was not made true by the API. It was made true by the receiver on the other end.

Remove the receiver, and the sender keeps emitting exactly the same signal. Nothing errors. Nothing goes red. The status code is still 502, the code still runs the same branch, the comment still makes the same promise — and the promise is now false. A submission that reaches neither of our sinks simply stops there.

Why nothing went red

This is the part worth generalising, because it is not a story about one careless change. Three separate safety nets all reported success:

  • Both repositories had green CI. Each one tested its own half correctly. The contract lived between them, and neither owned it.
  • A test existed for the failure path. It asserted the endpoint returns 502. That assertion passes whether the 502 is a handoff, a queue-and-retry, or a lead disappearing forever — so it could never have caught this.
  • Nobody's dashboard changed. Leads still arrive. This only bites during an outage, which is precisely when nobody is reading the number that stopped moving.

We found it days later, and only because a code reviewer chasing something unrelated stopped to ask who the 502 was actually talking to.

The rule we took away

When you remove a vendor, ask what it was doing besides the thing you removed it for.

Third-party components accumulate jobs nobody wrote down. The form relay was a PII risk, and it was also our retry queue, our outage buffer and our proof that a lead could not vanish. We audited it as one thing and removed it as one thing.

The same shape shows up everywhere in ordinary businesses. The plugin you dropped for being slow was also generating your sitemap. The analytics script you removed for GDPR was also the thing your uptime alert watched for. The old payment provider you replaced was also emailing customers their receipts. In every case the removal is correct and the second-order effect is invisible, because the thing that stops happening never announced that it was happening.

What we actually changed

We did not restore the fallback — a third-party PII path is not something you reinstate. We did three smaller things instead:

  1. Made the failure say its own name. A double-sink failure now emits a dedicated error line, INTAKE_LEAD_UNSUNK, carrying the submission. It keeps the contact details deliberately: a recovery copy you cannot reply from is not a recovery copy.
  2. Corrected the sentence. The documentation now describes what the code guarantees today, and names the card holding the decision about what should replace the fallback. A comment that promises something the code stopped doing is worse than no comment — people trust it.
  3. Wrote a test that can fail. Not "returns 502", but "the lead is still recoverable, and marked as needing recovery". We then broke the fix four different ways to confirm the test noticed each time. A guard you have never seen fail is not yet evidence of anything.

And we wrote down the part that is still missing, rather than calling it done: a marker with no alert attached is a red light in an empty room. The alerting policy is a separate change, on a separate card, and pretending otherwise would be the same mistake in a new place.

The five-minute version, for your next vendor removal

  • List everything that calls the component, not just what it does.
  • Search your codebase for promises in the vicinity — "never", "always", "guaranteed", "no data is lost". Any of them that the vendor was making true is now yours to keep or retract.
  • Ask what your error paths were talking to. An error code with no listener is not an error, it is a shrug.
  • Check whether your failure-path tests would pass in a world where the failure is unhandled. If they would, they are testing shape, not behaviour.
  • Do the removal in both repositories' heads at once. Contracts that span two services belong to neither one's CI.

None of this requires a bigger test suite. It requires asking, once, what a component was quietly holding up.

We build and maintain web platforms, mobile apps and AI agent systems at MSApps, and this is the kind of thing we look for when we take over an existing codebase — the guarantees that used to be true (you can see the kind of systems we run in our work). If you have a system where the safety nets have drifted, talk to us.

← All articles