# 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"