CodeSampleX

Ejemplo

axum 0.8.9: Protect axum routes with stateful middleware using from_fn_with_state, isolate route_layer from 404 fallbacks, and manage bottom-to-top layer execution order

Muestra verificada para cargo axum 0.8.9: Protect axum routes with stateful middleware using from_fn_with_state, isolate route_layer from 404 fallbacks, and…

sha256:0add9e86ea54fb151285ed7fff0ac6a073835d84159f6bfc43b6e447f030a7be

Esta red ofrece una sola cosa: una muestra que compila. La ejecutó en un sandbox y guardó el recibo firmado. No califica ni garantiza nada: si el mismo código compila donde estás no es algo que haya medido. Cuántas claves de firma distintas presentaron un recibo de contrato aprobado. Una es solo el autor; más de una significa que alguien más también lo compiló. Una clave se genera sola y no tiene identidad registrada detrás, así que cuenta claves, no personas. MIT-0

Evidencia de ejecución

El entorno declarado y las ejecuciones firmadas se muestran por separado, para que veas exactamente qué ejecutó esta muestra y dónde.

Base de evidencia
Contrato firmado aprobado
Recibos de verificación
3
Claves de firma que lo compilaron
2
Entorno declarado rust linux x64 rust rust cargo

Entornos de las ejecuciones de verificación

Entorno Contrato Etapas Ejecución
rust 1 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · cargo@1
2026-08-16
rust 1 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · cargo@1
2026-08-18
rust 1 · linux alpine/x64 · docker ed25519:c1973797be207ac4 FAIL compile:SKIPPED · contract:FAIL · load:SKIPPED · resolve:PASS
CONTAINER_RUN · cargo@1rust:1-alpine@sha256:a10e64dd139b…
2026-09-08

Caso

HOW
Objetivo
Protect axum routes with stateful middleware using from_fn_with_state, isolate route_layer from 404 fallbacks, and manage bottom-to-top layer execution order
Paquetes
Símbolos
  • axum::middleware::from_fn
  • axum::middleware::from_fn_with_state
  • axum::middleware::Next
  • axum::Router::route_layer
  • axum::Router::layer
  • axum::extract::Extension
  • axum::extract::State
  • axum::extract::Request
Entorno
rust
Creado
2026-08-16T08:11:01Z

Contrato

  1. assert route_layer protects routes registered before it, returning 200 with typed Extension and injected headers when authorization token is valid
  2. assert route_layer rejects unauthorized requests with 401 and custom error headers without invoking the route handler
  3. assert route_layer leaves public routes registered after it unprotected, returning 200 without executing the auth middleware or incrementing state call counter
  4. assert an unmatched 404 route on a route_layer router returns 404 Not Found without invoking auth middleware
  5. assert Router::layer wraps the entire router including 404 fallbacks, returning 401 Unauthorized for unmatched paths when authorization is absent
  6. assert an unpopulated Extension extractor fails dynamically at runtime with 500 Internal Server Error when the route is not wrapped by the populating middleware
  7. assert chained layers execute in reverse registration order (bottom-to-top): outer layer runs before inner layer on request, and inner layer runs before outer layer on response
  8. assert sub-router route_layer middleware is isolated to the nested prefix and does not run on parent router routes

Archivos

  • Cargo.lock
  • Cargo.toml
  • NOTES.md
  • csx.json
  • src/lib.rs
  • tests/middleware_tests.rs

Descargar el artefacto de código fuente (tar.gz)

Código fuente

Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"

[[package]]
name = "axum"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
 "axum-core",
 "bytes",
 "form_urlencoded",
 "futures-util",
 "http",
 "http-body",
 "http-body-util",
 "hyper",
 "hyper-util",
 "itoa",
 "matchit",
 "memchr",
 "mime",
 "percent-encoding",
 "pin-project-lite",
 "serde_core",
 "serde_json",
 "serde_path_to_error",
 "serde_urlencoded",
 "sync_wrapper",
 "tokio",
 "tower",
 "tower-layer",
 "tower-service",
 "tracing",
]

[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
 "bytes",
 "futures-core",
 "http",
 "http-body",
 "http-body-util",
 "mime",
 "pin-project-lite",
 "sync_wrapper",
 "tower-layer",
 "tower-service",
 "tracing",
]

[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"

[[package]]
name = "cargo-axum-api"
version = "0.1.0"
dependencies = [
 "axum",
 "http-body-util",
 "serde",
 "serde_json",
 "tokio",
 "tower",
]

[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
 "percent-encoding",
]

[[package]]
name = "futures-channel"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
dependencies = [
 "futures-core",
]

[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"

[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"

[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
 "futures-core",
 "futures-task",
 "pin-project-lite",
 "slab",
]

[[package]]
name = "http"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
dependencies = [
 "bytes",
 "itoa",
]

[[package]]
name = "http-body"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
dependencies = [
 "bytes",
 "http",
]

[[package]]
name = "http-body-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
dependencies = [
 "bytes",
 "futures-core",
 "http",
 "http-body",
 "pin-project-lite",
]

[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"

[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"

[[package]]
name = "hyper"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
dependencies = [
 "atomic-waker",
 "bytes",
 "futures-channel",
 "futures-core",
 "http",
 "http-body",
 "httparse",
 "httpdate",
 "itoa",
 "pin-project-lite",
 "smallvec",
 "tokio",
]

[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
 "bytes",
 "http",
 "http-body",
 "hyper",
 "pin-project-lite",
 "tokio",
 "tower-service",
]

[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"

[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"

[[package]]
name = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"

[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"

[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"

[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"

[[package]]
name = "mio"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
dependencies = [
 "libc",
 "wasi",
 "windows-sys",
]

[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"

[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"

[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"

[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
 "unicode-ident",
]

[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"

[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
 "serde_core",
 "serde_derive",
]

[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
 "itoa",
 "memchr",
 "serde",
 "serde_core",
 "zmij",
]

[[package]]
name = "serde_path_to_error"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [
 "itoa",
 "serde",
 "serde_core",
]

[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
 "form_urlencoded",
 "itoa",
 "ryu",
 "serde",
]

[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"

[[package]]
name = "smallvec"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"

[[package]]
name = "socket2"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
 "libc",
 "windows-sys",
]

[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"

[[package]]
name = "tokio"
version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
 "libc",
 "mio",
 "pin-project-lite",
 "socket2",
 "tokio-macros",
 "windows-sys",
]

[[package]]
name = "tokio-macros"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
 "futures-core",
 "futures-util",
 "pin-project-lite",
 "sync_wrapper",
 "tokio",
 "tower-layer",
 "tower-service",
 "tracing",
]

[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"

[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"

[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
 "log",
 "pin-project-lite",
 "tracing-core",
]

[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
 "once_cell",
]

[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"

[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"

[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"

[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
 "windows-link",
]

[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
Cargo.toml
[package]
name = "cargo-axum-api"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.8.9"
tower = { version = "0.5.3", features = ["util"] }
http-body-util = "0.1.5"
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
tokio = { version = "1.53.1", features = ["rt", "macros"] }
NOTES.md
# Notes on axum Middleware Layering and State Scoping

## search_known_solution Findings
`search_known_solution` found sample `sha256:ad2f9d5347cb52c3acc083de3c1608225248a42c41031bf843e1163d738b6e70` ("Test axum handlers and routing without binding a port by driving the whole Router through tower's ServiceExt::oneshot, and get the exact status for each way a request can be wrong"), which covers handler execution via `oneshot`, JSON parse error status codes (400 vs 422 vs 415), handler extractor argument order, and axum 0.8 path capture syntax (`{id}`).

## What a Model Would Have Written Instead
A model would have written `layer(axum::middleware::from_fn(auth_mw))` on a router expecting `State<AppState>` to be automatically injected from `.with_state(state)`, and assumed `.layer()` only guards valid matched endpoints while executing layers in top-to-bottom order.

## How the Wrong Version Fails
- `from_fn(auth_mw)` fails loudly at compile time (`error[E0277]: the trait FromRequestParts<()> is not implemented for State<AppState>` because `from_fn` provides unit state `()`).
- `.layer()` instead of `.route_layer()` fails silently with a green build at runtime, intercepting 404 fallbacks so unauthorized probes to non-existent routes return `401 Unauthorized` instead of `404 Not Found`.
- Chained `.layer(A).layer(B)` fails silently with a green build by executing layers in bottom-to-top order (`B` executes before `A` on requests).
csx.json
{"case":{"caseId":"case:sha256:c028088c4e63dd7bd49129a6cda4e84af8ce172b2a9710683a2fe04b5d5043ae","contract":["assert route_layer protects routes registered before it, returning 200 with typed Extension and injected headers when authorization token is valid","assert route_layer rejects unauthorized requests with 401 and custom error headers without invoking the route handler","assert route_layer leaves public routes registered after it unprotected, returning 200 without executing the auth middleware or incrementing state call counter","assert an unmatched 404 route on a route_layer router returns 404 Not Found without invoking auth middleware","assert Router::layer wraps the entire router including 404 fallbacks, returning 401 Unauthorized for unmatched paths when authorization is absent","assert an unpopulated Extension extractor fails dynamically at runtime with 500 Internal Server Error when the route is not wrapped by the populating middleware","assert chained layers execute in reverse registration order (bottom-to-top): outer layer runs before inner layer on request, and inner layer runs before outer layer on response","assert sub-router route_layer middleware is isolated to the nested prefix and does not run on parent router routes"],"goal":"Protect axum routes with stateful middleware using from_fn_with_state, isolate route_layer from 404 fallbacks, and manage bottom-to-top layer execution order","kind":"HOW","packages":["pkg:cargo/axum@0.8.9","pkg:cargo/tower@0.5.3","pkg:cargo/http-body-util@0.1.5","pkg:cargo/tokio@1.53.1"],"schemaVersion":1,"symbols":["axum::middleware::from_fn","axum::middleware::from_fn_with_state","axum::middleware::Next","axum::Router::route_layer","axum::Router::layer","axum::extract::Extension","axum::extract::State","axum::extract::Request"]},"contractCommand":["cargo","test","--offline"],"environment":{"arch":"x64","ecosystem":"cargo","executionContext":"rust","language":"rust","os":"linux","packageManager":"cargo","runtime":"rust","schemaVersion":1},"license":"MIT-0","packages":["pkg:cargo/axum@0.8.9","pkg:cargo/tower@0.5.3","pkg:cargo/http-body-util@0.1.5","pkg:cargo/tokio@1.53.1"],"schemaVersion":1,"symbols":["axum::middleware::from_fn","axum::middleware::from_fn_with_state","axum::middleware::Next","axum::Router::route_layer","axum::Router::layer","axum::extract::Extension","axum::extract::State","axum::extract::Request"],"verifierAdapter":"cargo@1"}
src/lib.rs
//! CodeSampleX sample for axum middleware layering and state scoping.
//! Proves the critical differences between `axum::middleware::from_fn_with_state` vs `from_fn`,
//! `Router::route_layer` vs `Router::layer` on 404 fallback isolation and route boundaries,
//! dynamic runtime failure of unpopulated `Extension` extractors, and bottom-to-top layer execution order.

use axum::{
    body::Body,
    extract::{Extension, Request, State},
    http::{header, HeaderMap, HeaderValue, Method, StatusCode},
    middleware::{from_fn, from_fn_with_state, Next},
    response::{IntoResponse, Response},
    routing::get,
    Router,
};
use http_body_util::BodyExt;
use std::sync::{
    atomic::{AtomicU32, Ordering},
    Arc,
};
use tower::ServiceExt;

#[derive(Clone)]
pub struct AppState {
    pub auth_token: &'static str,
    pub call_counter: Arc<AtomicU32>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthenticatedUser {
    pub user_id: String,
    pub role: String,
}

/// Stateful authentication middleware.
/// Requires `from_fn_with_state` because it extracts `State<AppState>`.
pub async fn auth_middleware(
    State(state): State<AppState>,
    mut req: Request,
    next: Next,
) -> Response {
    state.call_counter.fetch_add(1, Ordering::SeqCst);

    let auth_header = req
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok());

    match auth_header {
        Some(token) if token == state.auth_token => {
            // 1. Inject typed identity into request extensions for downstream extractors
            req.extensions_mut().insert(AuthenticatedUser {
                user_id: "user_42".to_string(),
                role: "admin".to_string(),
            });

            // 2. Inject mutated request header visible to handler's HeaderMap
            req.headers_mut().insert(
                "x-auth-injected",
                HeaderValue::from_static("authenticated"),
            );

            let mut res = next.run(req).await;

            // 3. Inject custom response header
            res.headers_mut().insert(
                "x-auth-status",
                HeaderValue::from_static("verified"),
            );
            res
        }
        _ => (
            StatusCode::UNAUTHORIZED,
            [("x-auth-error", "invalid-or-missing-token")],
            "Unauthorized",
        )
            .into_response(),
    }
}

/// Outer middleware layer to prove layer execution ordering.
pub async fn order_layer_outer(req: Request, next: Next) -> Response {
    let trace = req
        .extensions()
        .get::<Arc<std::sync::Mutex<Vec<&'static str>>>>()
        .cloned()
        .unwrap_or_default();
    trace.lock().unwrap().push("outer_in");

    let mut res = next.run(req).await;
    trace.lock().unwrap().push("outer_out");

    res.headers_mut()
        .insert("x-trace-outer", HeaderValue::from_static("outer"));
    res
}

/// Inner middleware layer to prove layer execution ordering.
pub async fn order_layer_inner(req: Request, next: Next) -> Response {
    let trace = req
        .extensions()
        .get::<Arc<std::sync::Mutex<Vec<&'static str>>>>()
        .cloned()
        .unwrap_or_default();
    trace.lock().unwrap().push("inner_in");

    let mut res = next.run(req).await;
    trace.lock().unwrap().push("inner_out");

    res.headers_mut()
        .insert("x-trace-inner", HeaderValue::from_static("inner"));
    res
}

// Handlers

pub async fn protected_handler(
    Extension(user): Extension<AuthenticatedUser>,
    headers: HeaderMap,
) -> (StatusCode, String) {
    let injected = headers
        .get("x-auth-injected")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("missing");
    (
        StatusCode::OK,
        format!("user={},role={},injected={}", user.user_id, user.role, injected),
    )
}

pub async fn public_handler() -> &'static str {
    "public content"
}

/// Handler requiring Extension<AuthenticatedUser>.
/// If placed on an unprotected route, this fails dynamically with 500.
pub async fn unguarded_extension_handler(
    Extension(user): Extension<AuthenticatedUser>,
) -> String {
    format!("Never reached for unauthenticated: {}", user.user_id)
}

// Router factories

pub fn build_route_layer_router(state: AppState) -> Router {
    // `route_layer` ONLY wraps routes registered BEFORE the call.
    // Routes registered AFTER `route_layer` bypass this middleware.
    Router::new()
        .route("/protected", get(protected_handler))
        .route_layer(from_fn_with_state(state.clone(), auth_middleware))
        .route("/public", get(public_handler))
        .route("/unprotected-needs-ext", get(unguarded_extension_handler))
        .with_state(state)
}

pub fn build_global_layer_router(state: AppState) -> Router {
    // `layer` wraps the ENTIRE router, including unmatched 404 fallbacks.
    Router::new()
        .route("/protected", get(protected_handler))
        .route("/public", get(public_handler))
        .layer(from_fn_with_state(state.clone(), auth_middleware))
        .with_state(state)
}

pub fn build_nested_router(state: AppState) -> Router {
    let sub_router = Router::new()
        .route("/admin", get(protected_handler))
        .route_layer(from_fn_with_state(state.clone(), auth_middleware));

    Router::new()
        .route("/root-public", get(public_handler))
        .nest("/api", sub_router)
        .with_state(state)
}

pub fn build_ordered_layers_router() -> Router {
    // In Tower / Axum: .layer(A).layer(B) means B wraps A.
    // B runs first on request (outer), then A (inner).
    Router::new()
        .route(
            "/order",
            get(|req: Request| async move {
                let trace = req
                    .extensions()
                    .get::<Arc<std::sync::Mutex<Vec<&'static str>>>>()
                    .cloned()
                    .unwrap();
                trace.lock().unwrap().push("handler");
                "order_ok"
            }),
        )
        .layer(from_fn(order_layer_inner))
        .layer(from_fn(order_layer_outer))
}

pub async fn execute_request(
    router: &Router,
    method: Method,
    uri: &str,
    auth_header: Option<&str>,
    extensions: Option<Arc<std::sync::Mutex<Vec<&'static str>>>>,
) -> (StatusCode, HeaderMap, String) {
    let mut builder = Request::builder().method(method).uri(uri);
    if let Some(token) = auth_header {
        builder = builder.header(header::AUTHORIZATION, token);
    }
    let mut req = builder.body(Body::empty()).expect("valid request");
    if let Some(ext) = extensions {
        req.extensions_mut().insert(ext);
    }

    let response = router
        .clone()
        .oneshot(req)
        .await
        .expect("oneshot execution cannot fail");

    let status = response.status();
    let headers = response.headers().clone();
    let bytes = response
        .into_body()
        .collect()
        .await
        .expect("collect body")
        .to_bytes();
    let body = String::from_utf8(bytes.to_vec()).expect("utf-8 body");

    (status, headers, body)
}
tests/middleware_tests.rs
use axum::http::{Method, StatusCode};
use cargo_axum_api::*;
use std::sync::{
    atomic::{AtomicU32, Ordering},
    Arc, Mutex,
};

#[tokio::test]
async fn test_route_layer_authorized_success() {
    let call_counter = Arc::new(AtomicU32::new(0));
    let state = AppState {
        auth_token: "Bearer token_secret_999",
        call_counter: call_counter.clone(),
    };
    let app = build_route_layer_router(state);

    let (status, headers, body) = execute_request(
        &app,
        Method::GET,
        "/protected",
        Some("Bearer token_secret_999"),
        None,
    )
    .await;

    assert_eq!(status, StatusCode::OK);
    assert_eq!(body, "user=user_42,role=admin,injected=authenticated");
    assert_eq!(
        headers.get("x-auth-status").unwrap().to_str().unwrap(),
        "verified"
    );
    assert_eq!(call_counter.load(Ordering::SeqCst), 1);
}

#[tokio::test]
async fn test_route_layer_unauthorized_rejection() {
    let call_counter = Arc::new(AtomicU32::new(0));
    let state = AppState {
        auth_token: "Bearer token_secret_999",
        call_counter: call_counter.clone(),
    };
    let app = build_route_layer_router(state);

    let (status, headers, body) = execute_request(
        &app,
        Method::GET,
        "/protected",
        Some("Bearer wrong_token"),
        None,
    )
    .await;

    assert_eq!(status, StatusCode::UNAUTHORIZED);
    assert_eq!(body, "Unauthorized");
    assert_eq!(
        headers.get("x-auth-error").unwrap().to_str().unwrap(),
        "invalid-or-missing-token"
    );
    assert_eq!(call_counter.load(Ordering::SeqCst), 1);
}

#[tokio::test]
async fn test_route_layer_bypasses_subsequent_routes() {
    let call_counter = Arc::new(AtomicU32::new(0));
    let state = AppState {
        auth_token: "Bearer token_secret_999",
        call_counter: call_counter.clone(),
    };
    let app = build_route_layer_router(state);

    // /public was registered AFTER route_layer, so middleware is completely bypassed
    let (status, headers, body) =
        execute_request(&app, Method::GET, "/public", None, None).await;

    assert_eq!(status, StatusCode::OK);
    assert_eq!(body, "public content");
    assert!(headers.get("x-auth-status").is_none());
    // Counter did NOT increment
    assert_eq!(call_counter.load(Ordering::SeqCst), 0);
}

#[tokio::test]
async fn test_route_layer_404_isolation() {
    let call_counter = Arc::new(AtomicU32::new(0));
    let state = AppState {
        auth_token: "Bearer token_secret_999",
        call_counter: call_counter.clone(),
    };
    let app = build_route_layer_router(state);

    // Requesting an unknown route with route_layer returns 404 WITHOUT executing auth middleware
    let (status, _, _) =
        execute_request(&app, Method::GET, "/non-existent-endpoint", None, None).await;

    assert_eq!(status, StatusCode::NOT_FOUND);
    assert_eq!(call_counter.load(Ordering::SeqCst), 0);
}

#[tokio::test]
async fn test_global_layer_intercepts_404() {
    let call_counter = Arc::new(AtomicU32::new(0));
    let state = AppState {
        auth_token: "Bearer token_secret_999",
        call_counter: call_counter.clone(),
    };
    let app = build_global_layer_router(state);

    // Global `.layer()` wraps the fallback handler: unauthorized request to unmatched URL gets 401, not 404!
    let (status, headers, body) =
        execute_request(&app, Method::GET, "/non-existent-endpoint", None, None).await;

    assert_eq!(status, StatusCode::UNAUTHORIZED);
    assert_eq!(body, "Unauthorized");
    assert_eq!(
        headers.get("x-auth-error").unwrap().to_str().unwrap(),
        "invalid-or-missing-token"
    );
    assert_eq!(call_counter.load(Ordering::SeqCst), 1);
}

#[tokio::test]
async fn test_unpopulated_extension_fails_with_500() {
    let call_counter = Arc::new(AtomicU32::new(0));
    let state = AppState {
        auth_token: "Bearer token_secret_999",
        call_counter: call_counter.clone(),
    };
    let app = build_route_layer_router(state);

    // `/unprotected-needs-ext` requires Extension<AuthenticatedUser> but is NOT wrapped by auth_middleware
    // Missing Extension causes Axum to return 500 Internal Server Error at runtime
    let (status, _, body) =
        execute_request(&app, Method::GET, "/unprotected-needs-ext", None, None).await;

    assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
    assert!(
        body.contains("Missing request extension")
            || body.contains("Extension of type")
            || body.contains("AuthenticatedUser"),
        "body was: {body}"
    );
}

#[tokio::test]
async fn test_layer_execution_order_trace() {
    let app = build_ordered_layers_router();
    let trace = Arc::new(Mutex::new(Vec::new()));

    let (status, headers, body) = execute_request(
        &app,
        Method::GET,
        "/order",
        None,
        Some(trace.clone()),
    )
    .await;

    assert_eq!(status, StatusCode::OK);
    assert_eq!(body, "order_ok");

    let recorded = trace.lock().unwrap().clone();
    // Verification that bottom-to-top order occurred:
    // .layer(inner).layer(outer) -> outer runs first, then inner, then handler, then inner, then outer
    assert_eq!(
        recorded,
        vec!["outer_in", "inner_in", "handler", "inner_out", "outer_out"]
    );
    assert_eq!(
        headers.get("x-trace-outer").unwrap().to_str().unwrap(),
        "outer"
    );
    assert_eq!(
        headers.get("x-trace-inner").unwrap().to_str().unwrap(),
        "inner"
    );
}

#[tokio::test]
async fn test_nested_subrouter_isolation() {
    let call_counter = Arc::new(AtomicU32::new(0));
    let state = AppState {
        auth_token: "Bearer token_secret_999",
        call_counter: call_counter.clone(),
    };
    let app = build_nested_router(state);

    // 1. Root public route is unaffected by sub-router's route_layer
    let (status, _, body) =
        execute_request(&app, Method::GET, "/root-public", None, None).await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(body, "public content");
    assert_eq!(call_counter.load(Ordering::SeqCst), 0);

    // 2. Nested route is protected by sub-router's route_layer
    let (status, _, body) = execute_request(
        &app,
        Method::GET,
        "/api/admin",
        Some("Bearer token_secret_999"),
        None,
    )
    .await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(body, "user=user_42,role=admin,injected=authenticated");
    assert_eq!(call_counter.load(Ordering::SeqCst), 1);
}

Seeder de origen

csx-seed