DEV Community

Fillip Kosorukov
Fillip Kosorukov

Posted on

The Script That Said Posted When It Had Failed

The Script That Said Posted When It Had Failed

I hit the second version while reviewing a small automation that sends a status note after a scheduled data job finishes. The job did the real work, built the message, called the notification endpoint, printed posted, and exited cleanly. In the logs, it looked finished. In the place where the message should have appeared, there was nothing.

The failing part was the success check.

The code treated "request attempted" as "message delivered." It looked roughly like this:

response = requests.post(url, json=payload, timeout=20)
print("posted")
Enter fullscreen mode Exit fullscreen mode

That is a tempting shape because it works during the first happy-path test. The endpoint accepts the request, a message appears, and the log says posted. But the log line is not attached to the endpoint's answer.

There is a smaller Python trap here too: requests.post() does not raise an exception for a 400 or 500 response unless the code calls response.raise_for_status() or checks the status explicitly. A rejected request can still produce a perfectly normal Response object. If the code never asks what the receiver said, stdout can declare success after a failure.

The first repair is not complicated:

response = requests.post(url, json=payload, timeout=20)
if response.status_code >= 400:
    raise RuntimeError(f"notification failed: HTTP {response.status_code}")
print("posted")
Enter fullscreen mode Exit fullscreen mode

That branch is better, but it is still not always enough. Some endpoints return a browser challenge, proxy page, or other non-API response with a 200 status. For jobs that matter, I want the code to check the response shape too: expected status, expected content type, and, when this endpoint is supposed to return one, a message id.

Why the mistake survives code review

This kind of bug is easy to miss because the code is short and the failure mode sits outside the program. You can read the file and see a network call. You can run it once and see the message appear. The line after the call feels like a reasonable completion marker.

But completion markers are only useful when they mark the real boundary. For this notification script, that boundary was the receiver's acceptance contract.

The same pattern shows up in other places:

  • a backup job prints "uploaded" after opening a file, before the upload result is checked
  • an import says "saved" after parsing input, before the database transaction commits
  • a queue worker says "sent" after handing work to a client library, before the provider accepts it
  • a sync script says "done" after writing a local file, before the remote copy is verified

Each one is a tiny vocabulary problem. The log uses a verb that belongs to the final state, but the code has only reached an intermediate state.

Make the log earn the verb

I now try to make every completion log answer two questions: what external system accepted the work, and what evidence did it return?

For a notification, that evidence might be a 200 or 204 plus a JSON body with an id. For an email provider, it might be a provider message id. For a file upload, it might be a checksum or object version. For a database write, it might be the committed row count.

This is especially important in jobs that run unattended. A person running a script manually will often notice that the expected output did not show up. A cron job will not. It will write its line to a log file and leave the next run to inherit the false state.

If the evidence does not exist, the log should use a weaker verb. "Attempted notification" is honest. "Posted" is not.

The shape I prefer

def post_status(url, payload):
    response = requests.post(url, json=payload, timeout=20)
    if response.status_code not in (200, 201):
        raise RuntimeError(
            f"status post rejected: HTTP {response.status_code}"
        )
    content_type = response.headers.get("content-type", "")
    if "application/json" not in content_type:
        raise RuntimeError("status post returned non-JSON response")
    data = response.json()
    message_id = data.get("id") or data.get("message_id")
    if not message_id:
        raise RuntimeError("status post accepted without a message id")
    return message_id
Enter fullscreen mode Exit fullscreen mode

If a provider returns 202 Accepted, I would treat that as a different contract: the work was queued somewhere else, so the script needs a follow-up handle or status check before it logs posted.

Then the scheduled job can make a clean promise:

message_id = post_status(url, payload)
print(f"posted {message_id}")
Enter fullscreen mode Exit fullscreen mode

Now the print line is allowed. It only runs after the receiver clears the specific contract this script cares about.

The point is not that every script needs a framework, retry queue, or elaborate observability layer. Small scripts can stay small. But a small script still needs a real success boundary. If that boundary is external, the check has to ask the external system what happened.

That one extra branch would have made the missing notification obvious in the first run.

Top comments (0)