DEV Community

Sirak
Sirak

Posted on

A timeout is not a failed publish: a small Python state machine

A publishing API times out. The tempting response is to retry. But a timeout tells you that the client did not receive a response; it does not prove that the server rejected the post.

Here is a small, deliberately in-memory Python sketch of the decision boundary. It is not a production publisher. Its job is to make one rule testable: uncertain delivery must not silently return to the send queue.

def deliver(item, send):
    if item["state"] != "approved":
        return
    item["state"] = "sending"
    try:
        receipt = send()
    except TimeoutError:
        item["state"] = "uncertain"
        return
    if receipt.get("chat_id") != item["target"]:
        item["state"] = "uncertain"
        return
    if not receipt.get("message_id"):
        item["state"] = "uncertain"
        return
    item["receipt"] = receipt
    item["state"] = "published"

calls = []
def timeout_after_request():
    calls.append("sent")
    raise TimeoutError()

item = {"state": "approved", "target": -100123}
deliver(item, timeout_after_request)
deliver(item, timeout_after_request)
assert item["state"] == "uncertain"
assert len(calls) == 1

wrong = {"state": "approved", "target": -100123}
deliver(wrong, lambda: {"chat_id": -100999, "message_id": 7})
assert wrong["state"] == "uncertain"

ok = {"state": "approved", "target": -100123}
deliver(ok, lambda: {"chat_id": -100123, "message_id": 8})
assert ok["state"] == "published"
Enter fullscreen mode Exit fullscreen mode

What this tests

The second call does not send again because the item is no longer approved. A response naming another destination is held for investigation. A matching destination and message ID produce a receipt. These tests exercise decisions; they do not establish real API permissions or network reliability.

In a durable implementation, persist the attempt before making the request, claim it atomically, and hold attempts left in sending after restart for reconciliation. The sketch above does none of that persistence. Also bind approval to a content revision, check per-channel pause immediately before delivery, and limit any explicitly eligible retry.

Rejection and uncertainty are different states

The Telegram Bot API documents the returned Message for a successful send. Its ResponseParameters include retry_after for flood control. An explicit rate-limit rejection can be treated according to that response; a lost response needs reconciliation instead.

Inspect the destination before deciding whether to retry an uncertain attempt. Record the message URL if it already exists. Do not infer success from a local function returning, and do not infer failure merely from a missing receipt.

For the broader editorial workflow, I wrote An AI publishing workflow needs a delivery receipt. This DEV version concentrates on the testable Python decision boundary.

Top comments (0)