Debug and automate
Build an API smoke test
Combine curl exit status, HTTP status, timing, and a small response assertion into one repeatable check.
A smoke test proves one critical path works, from where the caller sits. It should fail clearly, finish quickly, and never change production data.
Think of it as “is it up, really?” asked by a machine. Not “did something answer”, but “did the right thing answer, with the right content, in time”.
Let’s put together what we built in this module, exit codes, time bounds, and output control, into one script.
The check
Create smoke.sh:
#!/bin/bash
body=$(mktemp)
trap 'rm -f "$body"' EXIT
status=$(curl --silent --show-error --fail-with-body --max-time 10 --output "$body" --write-out '%{response_code}' https://example.org/) || exit 1
test "$status" = 200
grep -q 'Example Domain' "$body"
Every option earns its place.
--silent --show-error removes the progress meter but keeps real errors visible. --fail-with-body makes HTTP errors fail the command while still saving the response, so you can read what the server said. --max-time 10 guarantees the check finishes within ten seconds. --write-out '%{response_code}' puts the status code in a variable while the body lands in a temporary file.
Then two assertions. The status must be exactly 200, not just “not an error”. And the body must contain text we expect. grep -q exits non-zero when it’s missing.
That last check catches what the status code hides: a load balancer answering 200 with an empty or wrong page.
The trap line removes the temporary file whenever the script exits, on success or failure. Cleanup you don’t have to remember is cleanup that happens.
Prove it fails
A check you’ve never seen fail is a check you can’t trust. Run it healthy first:
chmod +x smoke.sh
./smoke.sh; echo "exit: $?"
You get exit: 0. Now break it, twice.
Change the URL to a hostname that doesn’t exist and run it again. curl prints Could not resolve host and the exit is 1, because || exit 1 fired.
Restore the URL and change the grep text to Wrong Text. No output this time, but the exit is still 1. Bash returns the status of the last command, and the content check failed.
Non-zero on every failure. That’s the contract. cron, CI, and monitoring systems all understand it without parsing any output.
Two boundaries
Use a read-only endpoint. This script runs often and unattended, so it must never create, update, or delete anything. A health route or a public page, never a POST.
And never print a secret-bearing response into shared logs. If the endpoint needs a token, pass it from the environment like we did in the bearer token lesson, and keep the output quiet.
My advice is to wire this into cron or CI and only alert on failure. When it passes, you hear nothing. That silence is the whole point.
Try this on your own project: point the script at your homepage, pick a phrase that only appears when the page rendered correctly, and run it from cron.
Lesson completed