HTTPower.Middleware.Dedup (HTTPower v0.16.0)

View Source

In-flight request deduplication to prevent duplicate operations.

This module prevents duplicate requests from causing duplicate side effects (e.g., double charges, duplicate orders) by tracking in-flight requests and sharing responses with identical concurrent requests.

How It Works

  1. Request Fingerprinting - Each request gets a hash based on method + URL + body
  2. In-Flight Tracking - First request executes normally, subsequent identical requests wait
  3. Response Sharing - When the first request completes, all waiting requests receive the same response
  4. Automatic Cleanup - Tracking data is automatically removed after configurable TTL

Use Cases

  • Prevent double charges from double-clicks on payment buttons
  • Prevent duplicate orders from retry storms or race conditions
  • Ensure idempotency for critical mutations (POST/PUT/DELETE)

Configuration

# Global configuration
config :httpower, :deduplicate,
  enabled: true,
  ttl: 5_000  # 5 seconds - how long to track in-flight requests

# Per-request configuration
HTTPower.post(url,
  body: payment_data,
  deduplicate: true
)

# Or with options
HTTPower.post(url,
  body: payment_data,
  deduplicate: [
    enabled: true,
    ttl: 10_000,
    key: "custom-dedup-key"  # Optional: override hash generation
  ]
)

States

  • :in_flight - Request currently executing, other identical requests will wait
  • :completed - Brief period after completion to catch race conditions (100-500ms)

Thread Safety

Uses ETS for thread-safe storage and GenServer for coordination.

Summary

Functions

Cancels an in-flight request (called on error/timeout).

Returns a specification to start this module under a supervisor.

Completes a request, storing the response and notifying waiters.

Attempts to deduplicate a request.

Feature callback for the HTTPower pipeline.

Generates a deduplication hash from request parameters.

Starts the request deduplicator GenServer.

Functions

cancel(request_hash)

@spec cancel(String.t()) :: :ok

Cancels an in-flight request (called on error/timeout).

Examples

HTTPower.RequestDeduplicator.cancel(request_hash)

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

complete(request_hash, response, config \\ [])

@spec complete(String.t(), any(), keyword()) :: :ok

Completes a request, storing the response and notifying waiters.

Examples

HTTPower.RequestDeduplicator.complete(request_hash, response, config)

deduplicate(request_hash, config \\ [])

@spec deduplicate(
  String.t(),
  keyword()
) ::
  {:ok, :execute} | {:ok, :wait, reference()} | {:ok, any()} | {:error, atom()}

Attempts to deduplicate a request.

Returns:

  • {:ok, :execute} - First occurrence, proceed with execution
  • {:ok, :wait, ref} - Duplicate request, wait for in-flight to complete
  • {:ok, response} - Request just completed, return cached response
  • {:error, reason} - Deduplication disabled or error occurred

Examples

case HTTPower.RequestDeduplicator.deduplicate(request_hash, config) do
  {:ok, :execute} ->
    # Execute the request
    execute_request()

  {:ok, :wait, ref} ->
    # Wait for in-flight request to complete
    await_response(ref)

  {:ok, response} ->
    # Use cached response from just-completed request
    {:ok, response}
end

handle_request(request, config)

Feature callback for the HTTPower pipeline.

Checks for duplicate requests and either executes, waits, or returns cached response.

Returns:

  • {:ok, request} with dedup info stored in private (first occurrence)
  • {:halt, response} if cached response available (short-circuit)
  • Waits and returns {:halt, response} for duplicate in-flight requests

Examples

iex> request = %HTTPower.Request{method: :post, url: "https://api.example.com/charge", body: "..."}
iex> HTTPower.Dedup.handle_request(request, [enabled: true])
{:ok, modified_request}

hash(method, url, body)

@spec hash(atom(), String.t(), String.t() | nil) :: String.t()

Generates a deduplication hash from request parameters.

Examples

hash = HTTPower.RequestDeduplicator.hash(:post, "https://api.com/charge", ~s({"amount": 100}))

start_link(opts \\ [])

Starts the request deduplicator GenServer.