HTTP · failure handling · Python

Where duplicate writes begin.

A server saves the ticket. The connection closes before its reply arrives. This local experiment measures what happens next under three different retry policies.

Blind retry

2 tickets

The first write succeeded. Repeating the POST created a second ticket.

Same key, provider support

1 ticket

The second request recovered the receipt for the original ticket.

Stop for review

1 ticket

The client has no receipt. It stops instead of guessing whether another write is safe.

These counts come from the local server's records. In the review case, the client cannot see that a ticket exists. A missing response does not establish whether the write happened.

The observed results

ScenarioRequestsTickets savedClient state
Blind retry22complete
Same key, provider support21complete
Stop for review11review required
Provider ignores the key22complete
Validation rejection10rejected

The key is a contract.

Reusing an operation key works here because the server remembers the original request and returns its receipt. The fourth case sends the same header to a server that ignores it. Two tickets are still created.

Review is a useful outcome.

When the client cannot establish that a retry is safe, it records an uncertain result. A real workflow can reconcile that operation before deciding to send another write.

Inspect the request traces

Blind retry
{
  "client": {
    "state": "complete",
    "attempts": 2,
    "ticket_id": "ticket-2",
    "events": [
      {
        "attempt": 1,
        "event": "outcome_uncertain",
        "error_type": "RemoteDisconnected"
      },
      {
        "attempt": 2,
        "event": "receipt_received",
        "http_status": 201
      }
    ]
  },
  "server": {
    "records": [
      {
        "ticket_id": "ticket-1",
        "payload": {
          "request_id": "example-001",
          "subject": "Move the workshop booking"
        }
      },
      {
        "ticket_id": "ticket-2",
        "payload": {
          "request_id": "example-001",
          "subject": "Move the workshop booking"
        }
      }
    ],
    "requests": [
      {
        "request": 1,
        "key": null,
        "write": true,
        "cache_hit": false,
        "result": "written_then_connection_closed"
      },
      {
        "request": 2,
        "key": null,
        "write": true,
        "cache_hit": false,
        "result": "written_then_receipt_sent"
      }
    ]
  }
}
Same key, provider support
{
  "client": {
    "state": "complete",
    "attempts": 2,
    "ticket_id": "ticket-1",
    "events": [
      {
        "attempt": 1,
        "event": "outcome_uncertain",
        "error_type": "RemoteDisconnected"
      },
      {
        "attempt": 2,
        "event": "receipt_received",
        "http_status": 200
      }
    ]
  },
  "server": {
    "records": [
      {
        "ticket_id": "ticket-1",
        "payload": {
          "request_id": "example-001",
          "subject": "Move the workshop booking"
        }
      }
    ],
    "requests": [
      {
        "request": 1,
        "key": "same-operation",
        "write": true,
        "cache_hit": false,
        "result": "written_then_connection_closed"
      },
      {
        "request": 2,
        "key": "same-operation",
        "write": false,
        "cache_hit": true,
        "result": "cached_receipt"
      }
    ]
  }
}
Stop for review
{
  "client": {
    "state": "review_required",
    "attempts": 1,
    "ticket_id": null,
    "events": [
      {
        "attempt": 1,
        "event": "outcome_uncertain",
        "error_type": "RemoteDisconnected"
      }
    ]
  },
  "server": {
    "records": [
      {
        "ticket_id": "ticket-1",
        "payload": {
          "request_id": "example-001",
          "subject": "Move the workshop booking"
        }
      }
    ],
    "requests": [
      {
        "request": 1,
        "key": null,
        "write": true,
        "cache_hit": false,
        "result": "written_then_connection_closed"
      }
    ]
  }
}
Provider ignores the key
{
  "client": {
    "state": "complete",
    "attempts": 2,
    "ticket_id": "ticket-2",
    "events": [
      {
        "attempt": 1,
        "event": "outcome_uncertain",
        "error_type": "RemoteDisconnected"
      },
      {
        "attempt": 2,
        "event": "receipt_received",
        "http_status": 201
      }
    ]
  },
  "server": {
    "records": [
      {
        "ticket_id": "ticket-1",
        "payload": {
          "request_id": "example-001",
          "subject": "Move the workshop booking"
        }
      },
      {
        "ticket_id": "ticket-2",
        "payload": {
          "request_id": "example-001",
          "subject": "Move the workshop booking"
        }
      }
    ],
    "requests": [
      {
        "request": 1,
        "key": "same-operation",
        "write": true,
        "cache_hit": false,
        "result": "written_then_connection_closed"
      },
      {
        "request": 2,
        "key": "same-operation",
        "write": true,
        "cache_hit": false,
        "result": "written_then_receipt_sent"
      }
    ]
  }
}
Validation rejection
{
  "client": {
    "state": "rejected",
    "attempts": 1,
    "ticket_id": null,
    "events": [
      {
        "attempt": 1,
        "event": "rejected",
        "http_status": 422
      }
    ]
  },
  "server": {
    "records": [],
    "requests": [
      {
        "request": 1,
        "key": "same-operation",
        "write": false,
        "cache_hit": false,
        "result": "validation_rejected"
      }
    ]
  }
}

Run it yourself

python -m unittest -v python run_demo.py

Python's standard library. Real HTTP on 127.0.0.1. Fictional tickets. The fixture's records and key cache live in memory for each case; this experiment measures response loss, not recovery from a database or machine crash.