DEV Community

Dakota Wu
Dakota Wu

Posted on

Pin Duplicate-Delivery Receipts Before You Extract a Webhook Alias

Consider a billing module that verified webhook signatures, parsed event JSON, and inserted a ledger row for every accepted delivery. During a routine cleanup, the verifier moved into a new file while duplicate detection stayed in the old one. A replayed event then inserted a second ledger row, because the new boundary never received the prior event id. Treat that replay as an illustrative composite for the workflow below, not as a measured incident from this account.

What actually broke

The failure was not a weak signature algorithm, and it was not a missing unit test for a happy path. Callers already relied on a quieter duplicate contract whenever the provider retried the same event id. An extract can keep every crypto check green and still break that contract at the new function boundary. That is why the first artifact is a receipt row, not a fresh package layout or a renamed client class.

Three rules worth pinning

Production logs for this kind of module often show three rules that no README states in a checkable form. A repeated event id must return the first outcome and must skip a second ledger write. Signature header names can arrive under more than one alias, and only a known alias may pass. Raw body bytes are hashed before JSON parsing, so key order must not change the stored receipt.

Those rules describe current behavior, not a claim that the behavior is the desired long-term design. A characterization pin freezes those rules so a later cleanup can change exactly one rule on purpose. If finance still sends an older alias, record that alias in the pin before anyone deletes the accepting branch.

Receipt rows before any move

Keep the first suite to four fixtures, and give each fixture one receipt row with four fields. Store the event id, the alias that matched, the SHA-256 of the raw body, and an outcome label. Use the labels accepted, duplicate, and rejected, and do not invent a fourth label until a log line requires it. Four rows are enough to reject an extract that drifts, and a larger harness can wait for a real fifth case.

Use the fixture set below for this pass, and do not widen that set while the alias extract is still open.

  1. A valid delivery with the primary signature alias and an event id the process has not seen.
  2. The same raw bytes and the same event id sent again, which must remain a duplicate.
  3. An unknown alias that must be rejected without inserting a ledger row or growing the seen-set.
  4. A known alias whose signature fragment does not match the raw body hash prefix.

Add a fifth row only after an incident shows a behavior these four fixtures do not already cover. Until that incident exists, extra rows mostly add review noise and hide the contract you meant to protect. If two incidents describe the same alias mismatch, extend the existing row instead of opening a second fixture file. The goal is a stable pin for this extract, not a growing archive of one-off provider quirks.

Proposed artifact

The Python below is a proposed, unexecuted example for a fictional ledger webhook, not a log of a real outage. It is not a measured run, not a production verifier, and not a report of customer traffic. The signature compare is a toy prefix check so the example can run without a vendor secret. Replace that compare with your real check only after the receipt fields are already pinned on the old path.

# proposed and unexecuted: pin receipts before moving code
import hashlib
from dataclasses import dataclass

ALIASES = ("x-signature", "x-ledger-signature")

@dataclass(frozen=True)
class Receipt:
    event_id: str
    alias_used: str
    body_sha256: str
    outcome: str

def receipt_for(headers, body: bytes, seen: set[str]) -> Receipt:
    alias = next((name for name in ALIASES if name in headers), "")
    digest = hashlib.sha256(body).hexdigest()
    event_id = headers.get("x-event-id", "")
    fragment = headers.get(alias, "") if alias else ""
    if not alias or fragment != digest[:16]:
        outcome = "rejected"
    elif event_id in seen:
        outcome = "duplicate"
    else:
        seen.add(event_id)
        outcome = "accepted"
    return Receipt(event_id, alias, digest, outcome)

def test_duplicate_preserves_first_outcome():
    seen: set[str] = set()
    body = b'{"amount":10,"currency":"USD"}'
    headers = {
        "x-event-id": "evt-7",
        "x-signature": hashlib.sha256(body).hexdigest()[:16],
    }
    first = receipt_for(headers, body, seen)
    second = receipt_for(dict(headers), body, seen)
    assert (first.outcome, second.outcome) == ("accepted", "duplicate")
    assert first.body_sha256 == second.body_sha256
    assert first.alias_used == "x-signature"
Enter fullscreen mode Exit fullscreen mode

Store expectations outside the assertion

Keep fixtures beside the test so a reviewer can open one directory and see every pinned byte. Store raw bodies as files, store headers as JSON, and keep expected receipts in a table the test loads. Do not recompute the expected hash inside the assertion when a stored digest can sit beside the body file. A stored digest makes a parser that reorders keys fail the pin, which is the drift you want to notice.

# proposed and unexecuted: load stored receipts, do not rebuild them in the assert
import json
from pathlib import Path

def load_cases(root: Path):
    rows = json.loads((root / "expected_receipts.json").read_text())
    for row in rows:
        body = (root / row["body_file"]).read_bytes()
        headers = json.loads((root / row["headers_file"]).read_text())
        yield headers, body, row["receipt"]
Enter fullscreen mode Exit fullscreen mode
{
  "body_file": "evt-7.body",
  "headers_file": "evt-7.headers.json",
  "receipt": {
    "event_id": "evt-7",
    "alias_used": "x-signature",
    "outcome": "accepted"
  }
}
Enter fullscreen mode Exit fullscreen mode

How to run the pin

Run one quiet pytest command, and treat any red assertion as a contract question rather than a prompt to edit the expected tuple. If the current module accepts an alias the test rejects, update the fixture from a captured request before you judge the extract. Reviewers should diff receipt fields, not the whole module, when they decide whether this pass may merge. A green run means these four observations still hold, and it says nothing about SQL, retries, or dashboard copy.

mkdir -p proposed/fixtures
python -m pytest proposed/test_delivery_receipts.py -q --tb=short
Enter fullscreen mode Exit fullscreen mode

How to read a failing receipt

A failing body hash means the bytes changed, or the hash moved onto parsed JSON instead of the raw input. A failing alias means the extracted function consulted a different header map than the fixture recorded. A failing outcome, with hash and alias still equal, usually means duplicate memory never crossed the new boundary. Fix the wiring that dropped the seen-set before you touch backoff numbers, SQL, or operator-facing error text.

Smallest safe extract

After the four receipts pass against the old module, move only the alias tuple into a new file. Leave hashing, the seen-set, and the ledger write in the original function for this pass. The new module exports the tuple and does not import the database, the JSON parser, or the logger. Name the new file for the table it holds, so reviewers do not expect a full client move.

# proposed and unexecuted: smallest extract is the alias table
SIGNATURE_ALIASES = ("x-signature", "x-ledger-signature")
Enter fullscreen mode Exit fullscreen mode

Point the old function at that tuple, then rerun the same four receipts without editing assertions. If the receipts match, stop the pull request there and write the follow-up as a separate change. A second pass may move the hash compare, but only after a mismatched-digest row is already in the pin. One exported tuple is a reviewable diff, while a relocated verifier plus a new retry loop is not.

This sequencing is deliberate scope control, not a claim that alias tables are the hardest part of webhook code. The hard part is knowing which behavior you promised not to change while the file is still messy. Keep the promise in the receipt table, and let the diff stay smaller than the module you are trying to leave behind. A reviewer should be able to explain this pass in one sentence without mentioning the database at all.

Where a separate host fits

Teams sometimes want a second machine for the first characterization run so the laptop that holds deploy keys is not also the test host. MonkeyCode offers free model access and a free server option for drafting redacted fixture rows and running this pytest file. Disclosure: This article was prepared as part of MonkeyCode's product outreach. This article does not state a token quota, a model name, a hardware shape, or a benchmark.

Those details belong in the current product documentation, and they can change without notice to this post. Paste any model draft beside the receipt table, and do not treat the draft as a captured request. Accept a new row only when a human can match its alias and outcome to a log line or a saved request. If those fixtures already live in git, the same free access and free server option can host this first off-laptop run.

Check the current limits in the product documentation before you depend on that run for a merge. Do not send signing secrets, live customer bodies, or unredacted payment identifiers to a hosted runner or a model prompt. The example above uses a fake event id, and a real verifier should stay on systems your policy already allows. A free server is a place to run the pin, not a place to terminate live provider webhooks.

Scope lock

Observation Pin in this pass Leave for a later pass
Duplicate event id keeps the first outcome Yes No
Which signature alias matched Yes No
Raw body hash recorded before JSON parsing Yes No
Ledger SQL and account mapping No Yes, after receipts stay green
Retry delays and backoff numbers No Yes, unless a fixture already encodes them
Operator-facing text for rejected events No Yes

If a requested edit is not in the pin column, it does not belong beside the alias extract. Split that edit, or the receipt suite can no longer tell you which change broke the row. The table is a review tool, not a ranking of which subsystem is more important to the business. Use it in the pull request description so the next reviewer does not reopen ledger SQL in this same diff.

Limitations and who should skip this

A pin preserves current behavior, including behavior that a later ticket may correctly call a defect. If the seen-set is only process memory, a duplicate after restart can still write a second row. This four-row test will not catch that gap, because both calls share one in-memory set. Fixing persistence is a separate change with its own fixture, not a drive-by edit inside the alias extract.

Skip the workflow when you have no captured request and would be inventing the contract from memory. Skip it when an external contract suite is already authoritative and your team treats that suite as the merge gate. Also skip a hosted runner when policy forbids third-party execution of repository code, even for a four-row file.

Model output is only a hypothesis generator, not a witness of what production actually sent last week. A draft can add a header that never arrived, or it can repair a duplicate path you meant only to record. The receipt comparison decides whether this pass may merge, and a fluent model explanation does not.

Review checklist

  1. Confirm each fixture came from a captured request or a log line, not from a model guess alone.
  2. Confirm the duplicate fixture reuses the same event id and the same raw bytes as the first delivery.
  3. Confirm the unknown-alias fixture expects rejected and asserts that the seen-set did not gain an id.
  4. Confirm the pull request diff touches the alias tuple and its import, not the ledger write.
  5. Confirm the pytest command in the change note is the same file that holds the four receipts.

What to merge this week

Write down the event id, the matched alias, the raw body hash, and the outcome label for four fixtures. Run that pin on the untouched module, extract only the alias tuple, and rerun the same assertions unchanged. Open the next cleanup only after that second run matches, even if the diff looks too small to matter. A small diff is the intended result when characterization comes first and the extract stays inside one seam.

Top comments (0)