Course outline

API Testing / Intermediate: interacting contracts / Lesson 10

Test retries and duplicate suppression

Model uncertain outcomes, scoped replay keys, and bounded retry decisions without sending requests.

About 4 min readContent reviewed
On this page

Lesson goals

  • Distinguish an unknown outcome from a known failed operation.
  • Test a fictional replay policy for same-key and changed-payload cases.

Begin with the uncertain outcome

A timeout means the caller did not receive a result within its deadline; it does not prove that the server performed no work. If a reading-list creation completed before the response was lost, an uncoordinated retry can create another entry. Separate failures before dispatch, failures with a known refusal, and failures after dispatch with an unknown outcome. The client needs a documented recovery strategy for each category. A blanket rule to retry every error confuses these states and can amplify both duplicate business actions and dependency pressure.

Specify the replay policy completely

An application replay key needs scope, payload binding, retention, and conflict behavior. This fictional policy scopes a key to an already trusted caller and operation, returns the stored result for an identical payload, and rejects reuse with a different payload. It does not claim a universal standardized header or a production implementation. The first program demonstrates the three cases in memory. In a real service, atomic storage and concurrent requests are essential: a separate check-then-insert sequence can allow two workers to perform the same action.

python A single-process fictional replay policy
saved = {}
def create(scope, key, book):
    slot = (scope, key)
    if slot in saved:
        old_book, result = saved[slot]
        return result if old_book == book else "KEY_PAYLOAD_CONFLICT"
    result = {"id": "entry-" + str(len(saved) + 1), "book": book}
    saved[slot] = (book, result)
    return result

first = create("reader-a:add", "lab-key-1", "book-4")
assert create("reader-a:add", "lab-key-1", "book-4") == first
assert create("reader-a:add", "lab-key-1", "book-5") == "KEY_PAYLOAD_CONFLICT"
assert len(saved) == 1
print("replay policy checked")
Expected output
replay policy checked

The complete program checks replay and payload conflict for synthetic values. The key is a public classroom marker, not a credential. There is no persistence, locking, expiry, HTTP header, or guarantee across processes.

Bound retries by outcome and safety

A retry decision depends on operation semantics, outcome knowledge, the replay mechanism, and remaining budget. An input error will not improve by waiting. A transient dependency failure might be retried when the operation can be repeated safely and a deadline still permits it. The second program is a decision table with a maximum attempt count, not a networking client. It deliberately excludes sleeping and real status handling. A real client must interpret its documented error categories, retry hints, cancellation behavior, and per-operation deadlines rather than treating this teaching policy as universal.

python A bounded local retry-decision model
def retry(category, repeat_safe, attempt):
    return category == "transient" and repeat_safe is True and attempt < 3

assert retry("transient", True, 1) is True
assert retry("input", True, 1) is False
assert retry("transient", False, 1) is False
assert retry("transient", True, 3) is False
print("four retry decisions checked")
Expected output
four retry decisions checked

The complete program verifies four declared decisions. It performs no delay, request, or load generation. The value three is an illustrative policy limit, not a recommended universal retry count.

Test the awkward lifecycle cases

Include duplicate delivery before completion, replay after completion, same key under a different caller scope, expired retention, and payload mismatch. Define whether a processing request returns an in-progress observation, waits within a bound, or refuses according to the chosen contract. Also define whether failed outcomes are retained. A stored replay response can itself contain private information, so scope and disclosure checks matter. Use controlled local interleavings or an approved disposable integration fixture; never create repeated requests against a configured service merely to see what happens.

Count business effects independently

The strongest replay evidence combines the returned result with the count of committed business effects. Receiving the same identifier twice is insufficient if two notifications or reservations occurred. Capture the state and relevant event observations, distinguish attempts from committed actions, and record the interruption point that produced the replay. An offline dictionary model can validate policy reasoning but cannot prove distributed exactly-once execution. Use precise language about duplicate suppression and the specific boundaries verified, including what happens when persistence or the downstream consumer fails.

Common errors and fixes

A timeout is treated as proof of no mutation

Why it happens
The caller confuses missing response with known server state.
How to fix it
Use the documented reconciliation or scoped replay mechanism.
Prevent it next time
Test lost-response cases separately from pre-dispatch failures.

A reused key silently accepts another book

Why it happens
The replay store does not bind the payload.
How to fix it
Compare the documented payload identity and return a conflict.
Prevent it next time
Include same-key changed-payload regression cases.

Retry attempts continue after the deadline

Why it happens
Attempt count and time budget are not coordinated.
How to fix it
Stop according to the operation deadline and cancellation policy.
Prevent it next time
Make retry budgets explicit and observable.

Check your understanding

1. Two identical replies are observed after a replay. What else must be checked?

Show worked solution

Check that the protected entry and relevant downstream effects occurred only as the contract allows. Equal response bodies can hide duplicate commits. Also record caller scope, payload binding, and retention assumptions so the result applies to a defined replay policy rather than every possible retry.

2. Should an invalid page-size request be retried three times?

Show worked solution

No under the declared policy. The input category needs correction by the caller, not a delay. Verify that the retry classifier returns false and that the client exposes a useful error. A transient classification should require documented evidence rather than being assigned to every failure.

Key takeaways

  • An unknown outcome requires reconciliation, not assumption.
  • Replay keys need scope, payload binding, and lifecycle rules.
  • Count committed effects as well as repeated responses.

Official references