reqkey 0.1.0

Official Rust SDK for ReqKey API key validation, credit metering, and analytics
Documentation
# Python SDK compatibility

This document records the implementation comparison against ReqKey Python SDK
`0.2.1`, which was the primary reference for Rust SDK `0.1.0`.

## Direct client

| Python behavior | Rust behavior | Status |
|---|---|---|
| `ReqKey` and `AsyncReqKey` | `SyncClient` and `Client` | Equivalent |
| `project_key` / `root_key` aliases | Same builder inputs and environment fallback | Equivalent |
| Default origin `https://api.reqkey.com` | Same | Equivalent |
| Two-second operation timeout | Same | Equivalent |
| `/key/validate` key/API/credits/resource payload | Same camel-case JSON | Equivalent |
| 200/402/403/429 are decisions | Same | Equivalent |
| 401 is project authentication error | `Error::Authentication` | Equivalent |
| Other/non-JSON responses are API errors | `Error::Api` | Equivalent |
| Timeout vs transport exceptions | Typed `Timeout` / `Transport` variants | Equivalent |
| Validation reason mapping | Same six stable categories | Equivalent |
| Retry delay from body, then header | Same | Equivalent |
| Full raw validation response | `VerificationResult::raw` | Equivalent |
| `/ingest` accepts 200 or 202 | Same | Equivalent |
| Full identity/header/query/body metadata | `IngestEventBuilder` | Equivalent |
| Body hard limit of 1,000 characters | Unicode-character limit of 1,000 | Equivalent |
| Python context-manager close | Reqwest clients close on Rust `Drop` | Idiomatic alternative |

Rust credit values use `u64`, making a negative value unrepresentable. Python
accepts runtime integers and raises for negative values. Both accept zero.

## Shared middleware policy

| Python behavior | Rust behavior | Status |
|---|---|---|
| `validate`, `ingest`, `both` | `Mode::{Validate, Ingest, Both}` | Equivalent |
| Enabled switch | `enabled(bool)` | Equivalent |
| Header/query/cookie keys | `KeyLocation` enum | Equivalent |
| Raw/Bearer schemes | `KeyScheme` enum; Bearer header-only | Equivalent |
| Custom key resolver | Framework-neutral closure | Equivalent |
| Static/dynamic credits | `credit_cost` / resolver | Equivalent |
| Exact and trailing-`*` exclusions | Same matching semantics | Equivalent |
| Default skip of OPTIONS | Same; replaceable method set | Equivalent |
| `should_protect` resolver | Framework-neutral predicate | Equivalent |
| Request-ID/path/name/IP resolvers | Same purpose | Equivalent |
| Custom denial messages | Typed `DenialCode` overrides | Equivalent |
| Denied ingestion in `both` | Enabled by default; configurable | Equivalent |
| Fail closed/open | `FailureMode` | Equivalent |
| Safe failure callback | `MiddlewareErrorEvent` | Equivalent |
| Callback errors do not affect response | Panics are caught and traced | Equivalent |
| Decision state and response headers | Native extension/guard types and same headers | Equivalent |
| Sensitive header/query redaction | Same defaults and configurable additions | Equivalent |
| Peer-first client-IP resolution | Same proxy fallback policy | Equivalent |
| Await ingestion before completion | Same in automatic/wrapped adapters | Equivalent |

Python permits sync or async resolver callables because framework requests and
Python callables are dynamically typed. Rust resolvers are synchronous,
thread-safe closures over the already-extracted `RequestContext`. They should
be cheap policy functions; application I/O belongs in the handler. This avoids
heap-allocated async callbacks and is the idiomatic Rust alternative.

Python's error callback may itself be async in ASGI/Django. Rust's callback is
synchronous and is intended for `tracing`, metrics, or sending to a nonblocking
channel. An alerting task can receive those events asynchronously.

## Framework adapters

### Axum

The Tower layer provides the closest one-to-one match with Python ASGI:
pre-handler validation, short-circuit denial, request extensions, response
headers, awaited ingestion, handler-error status ingestion, and bounded
textual response capture.

Axum capture is limited to uncompressed bodies with an exact known size no
larger than 4,004 bytes. Streaming/unknown-size bodies are forwarded untouched
and recorded without their body. Python ASGI can tap stream chunks directly;
Tower's erased Axum body requires a different wrapper design, so the bounded
known-body approach avoids buffering an arbitrary stream.

### Actix Web

The `Transform` implementation performs automatic validation, state exposure,
denials, response headers, and awaited analytics. It does not consume generic
or streaming `MessageBody` values. Response-body capture is therefore omitted;
all other configured metadata is preserved.

### Rocket

Rocket request fairings cannot terminate a request with a response. Protected
routes accept `ReqKeyGuard`, and `ReqKeyFairing` supplies shared state and the
post-response analytics hook. Optional scoped JSON catchers preserve ReqKey
denial bodies and retry headers. This route-guard design is the native Rocket
equivalent, not a literal ASGI middleware translation.

Rocket response bodies are not consumed or buffered by the response fairing.

### Warp

Warp is filter-oriented and has no general asynchronous after-response hook.
`ReqKeyFilter` validates and returns `WarpRequest`; handlers call its async
`record(reply)` wrapper. Omitting that wrapper intentionally results in
validation-only behavior for the response. The helper does not inspect bodies.

Warp 0.4 is included because it remains maintained and useful, but the explicit
reply wrapper is documented so developers do not assume invisible analytics.

## Request-body capture

The Python middleware does not capture incoming request bodies, although its
direct ingest client accepts them. Rust follows that safety behavior. Automatic
adapters never consume the request body before framework extractors. Plain or
custom integrations may opt in and supply a body through
`RequestContext::with_request_body`.

## Not carried over

- Python framework objects and dynamic attributes are replaced by typed
  extensions, guards, enums, and builders.
- Python context-manager methods are unnecessary because Rust clients release
  transports through RAII.
- Python async resolver/callback flexibility is replaced by synchronous policy
  hooks designed for request metadata only.
- No Python-specific ASGI/WSGI/Django lifecycle abstractions appear in the Rust
  public API.

These differences are intentional and keep the SDK native to Rust while
preserving ReqKey's observable API and authorization behavior.