2026-08-16 · 4 MIN

Your `curl | grep -q` health check fails when the page matches: pipefail, SIGPIPE, and page size

— WRITING

+

2026-08-16 · 4 MIN

A deploy script stopped deploying, and the reason was printed directly under the error message the whole time. The gate said this:

  it answered but the body does not say FetchDue. Title:
  <title>Invoice chasing software for agencies ... FetchDue</title>

The string it had just failed to find is in the line beneath the complaint. The probe was one line:

set -euo pipefail
is_fetchdue() { curl -s "http://localhost:$1/" --max-time 45 | grep -qi "FetchDue"; }

The failure is caused by the match

grep -q exits 0 the instant it sees the first match, and then it closes the pipe. curl is still writing the rest of the document into a reader that no longer exists, so curl exits non-zero. set -o pipefail makes the pipeline take the last non-zero status in it, which is curl's. The probe returns false, and it returns false precisely because the match was found early.

The size dependence is what makes this age into existence rather than show up in review. If curl finishes writing before grep is satisfied, nothing breaks. I reproduced both halves against a local http.server with the target string on line 1:

probe form300-byte page520,066-byte page
`curl -s ... \grep -qi under pipefail`exit 0exit 23
body=$(curl -s ...); grep -qi <<<"$body"exit 0exit 0

Exit 23 is curl's write error. The commit note records exit 56 for the same probe against the live host, where the transfer dies at a different layer. Neither is zero, which is all the shell cares about.

The live marketing page measures 357,757 bytes right now. It used to be small, and the probe used to work.

Invoice management interface displaying outstanding, unpaid, and upcoming invoices with amounts and dates — Financial metrics and complete invoice records with customer names, amounts, and payment status visible.

Invoice management interface displaying outstanding, unpaid, and upcoming invoices with amounts and dates — Financial metrics and complete invoice records with customer names, amounts, and payment status visible.

The fix is to stop making it a pipeline

is_fetchdue() {
  local body
  body=$(curl -s "http://localhost:$1/" --max-time "$PROBE_SECS" 2>/dev/null) || return 1
  grep -qi "FetchDue" <<<"$body"
}

Command substitution runs curl to completion, so its status is checked on its own terms. The here-string feeds grep from a buffer nobody can hang up on, and $? for the function belongs to grep. The two exit statuses stop being averaged together by a construct that was never asked to arbitrate between them.

Anywhere pipefail meets a short-circuiting reader, this applies. head, grep -q, grep -m1, read, an early exit in an awk script: all of them can finish before the producer does, and all of them turn the producer's death into the pipeline's verdict.

Three failures, three completely different causes, one sentence

This gate walks fourteen workspace views in a headless browser and blocks the deploy on any that fail. It had failed twice before this, both times reporting something about the application:

runwhat the gate printedwhat was actually wrong
2026-08-1510 of 14 views failed, every one "no .fd-shell-layout"Port 3000 was TriageDesk, started by another session. It vendors the same shell, so 4 views passed against a stranger's page
2026-08-16, first"never identified as FetchDue"The probe itself triggers a cold next dev compile, and its 10s timeout was shorter than that compile. The dev log showed GET / 200 up to the second the gate gave up
2026-08-16, second"it answered but the body does not say FetchDue"The body did say it. pipefail reported curl's broken pipe instead of grep's match

The 08-15 run is the one that should worry you most. Four views passed on the wrong application, so the gate could have greenlit a deploy on somebody else's measurements just as easily as it blocked ours.

What changed after the second run was the failure message. "Never identified" was covering two states that need different responses, so the failure branch now runs one more probe and prints the HTTP code, the byte count, and the page's <title>, then splits on whether the code is 000:

if [ "$probe_code" = "000" ]; then
  echo "  it never answered ... the dev compile is still running or the server died." >&2
else
  echo "  it answered but the body does not say FetchDue. Title:" >&2
  grep -o '<title>[^<]*</title>' /tmp/fd-gate-probe.html | head -1 >&2
fi

That diagnosis block is what printed the title next to the complaint, which is how the pipefail bug got found the same morning it was written.

That's how we built FetchDue.


One shipped product, taken apart, once a month. What it does, what it cost to build, what the pipeline behind it looks like, and what the numbers did — read off the repository and the live site, not written from memory. Join the list.

← All writing