Cucumber Specifications
These Cucumber specifications separate what draft-ietf-httpapi-idempotency-key-header-07 requires from the behaviour this project chose. Every scenario sits in a Rule tagged either @draft, which names the draft section it implements, or @library, which marks behaviour the draft leaves open. All idempot.dev projects share them.
Overview
13 of the 49 scenarios trace to the draft. The other 36 cover policy this project chose, and the Rule a scenario sits in says which it is.
Draft requirements:
- Section 2.1 — the key value is a String (1 scenario)
- Section 2.6 — a first request is processed, a duplicate replays the result, no resource is created twice (5 scenarios)
- Section 2.7 — 400, 422 and 409 with problem documents carrying type, title and detail (7 scenarios)
Library policy:
- Key validation and length limits — the 21 and 255 character limits, comma rejection, empty keys (12 scenarios)
- What the library records and replays — the stored record, the replayed status and body, the replay marker (5 scenarios)
- Fingerprint conflict detection — the same body under a different key answered with 409 (6 scenarios)
- Problem document extension members — instance, retryable and idempotency_key (5 scenarios)
- Store failures — 503 with a retryable problem document (3 scenarios)
- Method scope and edge cases — POST and PATCH protected, already-idempotent methods bypassed (5 scenarios)
Download
Download idempotency.feature (raw) — Use in your test suite
Specification
gherkin
# Every scenario belongs to exactly one Rule. A Rule tagged @draft states a
# requirement of draft-ietf-httpapi-idempotency-key-header-07 and carries the
# draft section in a @section tag. A Rule tagged @library states behaviour this
# library chose, which the draft does not specify. Tags are inherited, so a
# scenario must never carry a class tag of its own.
Feature: Idempotency-Key Header Compliance
As a developer implementing the IETF Idempotency-Key Header specification
I want to verify my middleware complies with the spec
So that my API handles idempotent requests correctly
Background:
Given a POST endpoint at "/api" that creates an order
And the SQLite store is clean
@draft @section:2.1
Rule: Draft section 2.1 - The key value is a String
The draft requires the Idempotency-Key field to be an Item Structured Header
whose value is a String. Accepting a well-formed key is the observable half of
that requirement.
Scenario: Valid Idempotency-Key is accepted
Given an Idempotency-Key "8e03978e-40d5-43e8-bc93-6894a57f9324"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 200
@draft @section:2.6
Rule: Draft section 2.6 - Idempotency enforcement
The draft requires the resource to process a request it has not seen and to
respond with the result of the previously completed operation when the same
key and fingerprint arrive again.
Scenario: First request creates expected resource
Given an Idempotency-Key "abc123456789012345678"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then 1 orders should exist in the database
Scenario: First request returns correct response body
Given an Idempotency-Key "abc123456789012345678"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 200
And the response body should contain "success"
Scenario: Duplicate request returns cached response
Given an Idempotency-Key "abc123456789012345678"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 200
And the response body should contain "success"
Scenario: Duplicate request does not duplicate resource creation
Given an Idempotency-Key "abc123456789012345678"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then 1 orders should exist in the database
Scenario: Multiple duplicate requests return cached response
Given an Idempotency-Key "abc123456789012345678"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
When I send a POST request to "/api" with body '{"foo":"bar"}'
And I send a POST request to "/api" with body '{"foo":"bar"}'
And I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 200
And 1 orders should exist in the database
@draft @section:2.7
Rule: Draft section 2.7 - Error handling
The draft requires a 400 when the key is missing, a 422 when a key is reused
with a different payload, and a 409 when a retry arrives before the original
completes. Its examples show each of those carried in an RFC 9457 problem
document with a type, a title and a detail.
Scenario: Missing Idempotency-Key header on POST returns 400
When I send a POST request to "/api" without an Idempotency-Key header
Then the response status should be 400
And the response content-type should be "application/problem+json"
And the response body should contain "section-2.1"
Scenario: Same key with different body returns 422
Given an Idempotency-Key "abc123456789012345678"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
When I send a POST request to "/api" with body '{"baz":"qux"}'
Then the response status should be 422
And the response content-type should be "application/problem+json"
And the response body should contain "section-2.2"
Scenario: Key reuse conflict response contains error message
Given an Idempotency-Key "abc123456789012345678"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
When I send a POST request to "/api" with body '{"baz":"qux"}'
Then the response status should be 422
And the response content-type should be "application/problem+json"
And the response body should contain "section-2.2"
And the response body should contain "title"
Scenario: Request during processing returns 409
Given an Idempotency-Key "abc123456789012345678"
And the key "abc123456789012345678" is currently being processed
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 409
And the response content-type should be "application/problem+json"
And the response body should contain "section-2.6"
Scenario: Concurrent request response contains error message
Given an Idempotency-Key "abc123456789012345678"
And the key "abc123456789012345678" is currently being processed
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 409
And the response content-type should be "application/problem+json"
And the response body should contain "section-2.6"
And the response body should contain "processed"
Scenario: Missing key error follows RFC 9457
When I send a POST request to "/api" without an Idempotency-Key header
Then the response status should be 400
And the response content-type should be "application/problem+json"
And the response body should contain "type"
And the response body should contain "title"
And the response body should contain "detail"
Scenario: Key reuse error follows RFC 9457
Given an Idempotency-Key "abc123456789012345678"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
When I send a POST request to "/api" with body '{"baz":"qux"}'
Then the response status should be 422
And the response content-type should be "application/problem+json"
And the response body should contain "type"
And the response body should contain "title"
And the response body should contain "detail"
@library
Rule: Library policy - Key validation and length limits
The draft fixes the syntax of the value but not its length, and it permits any
character an sf-string permits, commas included. The 21-character minimum, the
255-character maximum, the comma rejection and the empty-key rejection are this
library's policy, as are the RFC 9457 extension members asserted below.
Scenario: Missing key error includes RFC 9457 instance field
When I send a POST request to "/api" without an Idempotency-Key header
Then the response status should be 400
And the response body should contain "instance"
And the response body instance field should match pattern "urn:uuid:"
Scenario: Missing key error includes RFC 9457 retryable field
When I send a POST request to "/api" without an Idempotency-Key header
Then the response status should be 400
And the response body should contain "retryable"
And the response retryable field should be false
Scenario: Empty Idempotency-Key header returns 400
Given an empty Idempotency-Key header
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 400
And the response content-type should be "application/problem+json"
And the response body should contain "section-2.1"
And the response body should contain "type"
And the response body should contain "title"
And the response body should contain "detail"
Scenario: Empty key error includes RFC 9457 instance field
Given an empty Idempotency-Key header
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 400
And the response body should contain "instance"
And the response body instance field should match pattern "urn:uuid:"
Scenario: Empty key error includes RFC 9457 retryable field
Given an empty Idempotency-Key header
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 400
And the response body should contain "retryable"
And the response retryable field should be false
Scenario: Empty key error includes idempotency_key extension
Given an empty Idempotency-Key header
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 400
And the response body should contain "idempotency_key"
Scenario: Idempotency-Key exceeding 255 characters returns 400
Given an Idempotency-Key of 256 characters
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 400
And the response content-type should be "application/problem+json"
And the response body should contain "section-2.1"
And the response body should contain "type"
And the response body should contain "title"
And the response body should contain "detail"
Scenario: Too long key error includes RFC 9457 instance field
Given an Idempotency-Key of 256 characters
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 400
And the response body should contain "instance"
And the response body instance field should match pattern "urn:uuid:"
Scenario: Too long key error includes RFC 9457 retryable field
Given an Idempotency-Key of 256 characters
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 400
And the response body should contain "retryable"
And the response retryable field should be false
Scenario: Idempotency-Key with commas returns 400
Given an Idempotency-Key "key,with,commas,longer-than-twenty-chars"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 400
And the response content-type should be "application/problem+json"
And the response body should contain "section-2.1"
And the response body should contain "type"
And the response body should contain "title"
And the response body should contain "detail"
Scenario: Invalid key with commas includes RFC 9457 instance field
Given an Idempotency-Key "key,with,commas,longer-than-twenty-chars"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 400
And the response body should contain "instance"
And the response body instance field should match pattern "urn:uuid:"
Scenario: Invalid key with commas includes RFC 9457 retryable field
Given an Idempotency-Key "key,with,commas,longer-than-twenty-chars"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 400
And the response body should contain "retryable"
And the response retryable field should be false
@library
Rule: Library policy - What the library records and replays
The draft asks for the previous result to be returned but does not say how it
is kept. The record itself, its status field, the replayed status and body, and
the x-idempotent-replayed marker are this library's mechanism.
Scenario: First request creates idempotency record
Given an Idempotency-Key "abc123456789012345678"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then an idempotency record should exist with key "abc123456789012345678"
And the idempotency record status should be "complete"
Scenario: First request stores response status
Given an Idempotency-Key "abc123456789012345678"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the idempotency record response status should be 200
Scenario: First request stores response body
Given an Idempotency-Key "abc123456789012345678"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the idempotency record response body should contain "success"
Scenario: Duplicate request sets replay header
Given an Idempotency-Key "abc123456789012345678"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response should have header "x-idempotent-replayed" with value "true"
Scenario: Duplicate request does not create additional idempotency records
Given an Idempotency-Key "abc123456789012345678"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then 1 idempotency records should exist with key "abc123456789012345678"
@library
Rule: Library policy - Fingerprint conflict detection
The draft leaves uniqueness to the resource owner and never maps a request that
reuses a payload under a different key to a status. Answering it with 409 and a
section-2.2 problem type is this library's choice.
Scenario: Different key with same body returns 409
Given an Idempotency-Key "key1-12345678901234567"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
And an Idempotency-Key "key2-12345678901234567"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 409
And the response content-type should be "application/problem+json"
And the response body should contain "section-2.2"
Scenario: Fingerprint conflict response contains error message
Given an Idempotency-Key "key1-12345678901234567"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
And an Idempotency-Key "key2-12345678901234567"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 409
And the response content-type should be "application/problem+json"
And the response body should contain "section-2.2"
And the response body should contain "title"
Scenario: Fingerprint conflict does not create duplicate resources
Given an Idempotency-Key "key1-12345678901234567"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
And an Idempotency-Key "key2-12345678901234567"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 409
And the response content-type should be "application/problem+json"
And the response body should contain "section-2.2"
And 1 orders should exist in the database
Scenario: Fingerprint conflict includes RFC 9457 instance field
Given an Idempotency-Key "key1-12345678901234567"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
And an Idempotency-Key "key2-12345678901234567"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 409
And the response body should contain "instance"
And the response body instance field should match pattern "urn:uuid:"
Scenario: Fingerprint conflict includes RFC 9457 retryable field
Given an Idempotency-Key "key1-12345678901234567"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
And an Idempotency-Key "key2-12345678901234567"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 409
And the response body should contain "retryable"
And the response retryable field should be false
Scenario: Fingerprint conflict error follows RFC 9457
Given an Idempotency-Key "key1-12345678901234567"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
And an Idempotency-Key "key2-12345678901234567"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 409
And the response content-type should be "application/problem+json"
And the response body should contain "type"
And the response body should contain "title"
And the response body should contain "detail"
@library
Rule: Library policy - Problem document extension members
RFC 9457 permits extension members. Adding instance, retryable and
idempotency_key to the draft's error bodies is this library's choice, as is
asserting them for the statuses the draft does define.
Scenario: Key reuse conflict includes RFC 9457 instance field
Given an Idempotency-Key "abc123456789012345678"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
When I send a POST request to "/api" with body '{"baz":"qux"}'
Then the response status should be 422
And the response body should contain "instance"
And the response body instance field should match pattern "urn:uuid:"
Scenario: Key reuse conflict includes RFC 9457 retryable field
Given an Idempotency-Key "abc123456789012345678"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
When I send a POST request to "/api" with body '{"baz":"qux"}'
Then the response status should be 422
And the response body should contain "retryable"
And the response retryable field should be false
Scenario: Key reuse conflict includes idempotency_key extension
Given an Idempotency-Key "abc123456789012345678"
And I previously sent a POST request to "/api" with body '{"foo":"bar"}'
When I send a POST request to "/api" with body '{"baz":"qux"}'
Then the response status should be 422
And the response body should contain "idempotency_key"
And the response idempotency_key field should be "abc123456789012345678"
Scenario: Concurrent request includes RFC 9457 instance field
Given an Idempotency-Key "abc123456789012345678"
And the key "abc123456789012345678" is currently being processed
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 409
And the response body should contain "instance"
And the response body instance field should match pattern "urn:uuid:"
Scenario: Concurrent request includes RFC 9457 retryable field
Given an Idempotency-Key "abc123456789012345678"
And the key "abc123456789012345678" is currently being processed
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 409
And the response body should contain "retryable"
And the response retryable field should be true
@library
Rule: Library policy - Store failures
The draft does not cover an unavailable store. Answering 503 with a problem
document, and marking it retryable, is this library's choice.
Scenario: Store unavailable returns 503
Given the idempotency store is unavailable
And an Idempotency-Key "abc123456789012345678"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 503
And the response content-type should be "application/problem+json"
Scenario: Store unavailable includes RFC 9457 instance field
Given the idempotency store is unavailable
And an Idempotency-Key "abc123456789012345678"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 503
And the response body should contain "instance"
And the response body instance field should match pattern "urn:uuid:"
Scenario: Store unavailable includes RFC 9457 retryable field
Given the idempotency store is unavailable
And an Idempotency-Key "abc123456789012345678"
When I send a POST request to "/api" with body '{"foo":"bar"}'
Then the response status should be 503
And the response body should contain "retryable"
And the response retryable field should be true
@library
Rule: Library policy - Method scope and edge cases
The draft never states which methods a resource should protect. That POST and
PATCH are processed, and that the already-idempotent methods are not, follows
from RFC 9110's method list rather than from the draft.
Scenario: GET requests bypass idempotency processing
Given an Idempotency-Key "abc123456789012345678"
When I send a GET request to "/api"
Then the response status should be 200
Scenario: PUT requests bypass idempotency processing
Given an Idempotency-Key "abc123456789012345678"
When I send a PUT request to "/api" with body '{"foo":"bar"}'
Then the response status should be 200
Scenario: DELETE requests bypass idempotency processing
Given an Idempotency-Key "abc123456789012345678"
When I send a DELETE request to "/api"
Then the response status should be 200
Scenario: Empty request body is handled
Given an Idempotency-Key "abc123456789012345678"
When I send a POST request to "/api" with empty body
Then the response status should be 200
Scenario: PATCH requests are protected by idempotency
Given a PATCH endpoint at "/api" that creates an order
And an Idempotency-Key "abc123456789012345678"
When I send a PATCH request to "/api" with body '{"foo":"bar"}'
Then the response status should be 200
And an idempotency record should exist with key "abc123456789012345678"