Install
$ agentstack add skill-anantbhandarkar-make-it-right-mir-backend-rust-axum ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
/mir-backend-rust-axum · Make It Right (Axum)
Bottom tier of the chain: mir-backend (generic gates) → mir-backend-rust (Tokio runtime model) → this (Axum/Tower library mechanics). Run the gates first; load the Rust runtime tier for blocking/cancellation/Arc concerns; reach for this at Gate 5 (design mechanics), Gate 6 (implementation), and Gate 7 review. Runtime-level concerns (blocking the Tokio runtime, guard-across-await, cancellation safety, bounded channels, timeouts) live in mir-backend-rust — not here.
Stack assumed: Axum (current stable, 0.7+) · Tower middleware · async Rust on Tokio. Notes apply to axum with tokio and tower/tower-http.
The Axum footguns AI walks into most
1. Extractor order — body-consuming extractors MUST be last
This is the #1 Axum compile/runtime trap. HTTP request bodies are streams: reading the body consumes it. Axum extractors that consume the body (Json, String, Bytes, Form, Multipart) must be the last parameter in the handler signature. Only one body-consuming extractor per handler is allowed — the request has one body.
Extractors that do NOT consume the body (they read headers, URI, path, query, or app state) can appear in any position: Path, Query, State, Extension, TypedHeader, ConnectInfo.
// WRONG — Json is not last; compilation may succeed but the extractor
// that follows Json will see an empty/already-consumed body
async fn create_user(
Json(payload): Json, // consumes the body
State(db): State, // body already gone if Json is first
) -> impl IntoResponse { ... }
// RIGHT — body-consuming extractor is last
async fn create_user(
State(db): State, // reads app state, no body access
Json(payload): Json, // LAST — consumes the body here
) -> impl IntoResponse { ... }
// RIGHT — path + query before body
async fn update_item(
Path(id): Path,
Query(params): Query,
State(db): State,
Json(body): Json, // LAST
) -> impl IntoResponse { ... }
AI routinely puts State last (habit from other frameworks) and bumps Json to first or second — flag any handler where a body extractor is not the final argument.
2. State vs Extension — prefer State, use FromRef for sub-states
Axum provides two mechanisms for injecting app-wide data into handlers:
State(via.with_state(value)on the router): type-checked at compile time, zero-cost extraction, the idiomatic modern approach. Use this.Extension(via.layer(Extension(value))): stored as a type-erased map in the request extensions, extracted at runtime — a missing extension panics at runtime, not compile time. Use only for middleware-injected data (e.g. authenticated user injected by an auth layer downstream handlers expect).
For large apps with multiple shared resources (DB pool, config, cache), define a single AppState struct and implement FromRef for each sub-state so individual handlers can extract only what they need:
#[derive(Clone)]
struct AppState {
db: PgPool,
cache: RedisPool,
}
// Allow handlers to extract State directly from AppState
impl FromRef for PgPool {
fn from_ref(state: &AppState) -> Self {
state.db.clone()
}
}
// Handler only declares what it needs
async fn get_user(
State(db): State, // extracted via FromRef
Path(id): Path,
) -> impl IntoResponse { ... }
// Router wired to the full AppState
let app = Router::new()
.route("/users/:id", get(get_user))
.with_state(AppState { db, cache });
AppState must implement Clone (cheaply — wrap expensive resources in Arc or use pool handles that are already Clone).
3. Error handling — implement IntoResponse, never unwrap in handlers
Handlers return Result where E: IntoResponse. Do not .unwrap() or .expect() inside a handler — a panic kills the task and returns a 500 with no body (or crashes the process depending on panic handler). Implement IntoResponse for your error type to produce consistent, controlled HTTP responses:
#[derive(Debug)]
enum AppError {
NotFound(String),
DbError(sqlx::Error),
Unauthorized,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
AppError::DbError(e) => {
tracing::error!("db error: {e}");
(StatusCode::INTERNAL_SERVER_ERROR, "internal error".into())
}
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".into()),
};
(status, Json(json!({ "error": message }))).into_response()
}
}
async fn get_user(
State(db): State,
Path(id): Path,
) -> Result, AppError> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id as i64)
.fetch_optional(&db)
.await
.map_err(AppError::DbError)?
.ok_or_else(|| AppError::NotFound(format!("user {id} not found")))?;
Ok(Json(user))
}
Using anyhow::Error as the error type with a blanket impl IntoResponse is also valid for rapid development, but always ensure internal error details are not leaked to clients in production.
4. Tower middleware layer order — outermost wraps first, responses last
Tower middleware is composed as a stack: the layer added outermost (last in the builder chain or added first to ServiceBuilder) sees the request first and the response last. This is the reverse of what many frameworks call "middleware order."
let app = Router::new()
.route("/", get(handler))
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http()) // outermost: sees req first, resp last
.layer(TimeoutLayer::new(Duration::from_secs(10)))
.layer(CompressionLayer::new())
.layer(auth_layer) // innermost: sees req last, resp first
);
Practical ordering rules:
- Tracing/logging — outermost so it captures the full round trip including all other middleware latency.
- Timeout — before auth/business logic so runaway requests are cut regardless of what's inside.
- Auth/authorization — inner, after trace so requests are logged even if they fail auth (useful for security auditing), before business handlers.
- Compression — innermost (wraps the response body), since it should apply to the final handler output.
AI frequently reverses the intuition ("last added = outermost") because it matches neither Express nor Django middleware mental models. When reviewing a .layer() chain, trace through request and response direction explicitly.
How this slots into the core pipeline
- Gate 5 (Design): state handler signatures with correct extractor order; define
AppStatewithFromRefsub-states; declare the error type implementingIntoResponse; sketch the Tower middleware stack with ordering rationale. - Gate 6 (Implementation): body-consuming extractor last; use
StatenotExtensionfor app state;?propagation throughAppError: IntoResponse; check middleware layer ordering in theServiceBuilderchain. - Gate 7 (Review): verify extractor ordering in every handler; confirm no
unwrapin handlers; confirmStatevsExtensionusage; trace middleware request/response direction.
Edit boundary (what belongs here vs. above/below)
This module holds ONLY Axum + Tower library mechanics. Apply the 3-tier placement test before adding anything:
- True for Go/Python/Node too (idempotency, invariants, gates, observability)? → generic core (
mir-backend). - True for every async Rust backend on Tokio (blocking, guard-across-await, cancellation, Arc/'static, channels, timeouts)? → runtime tier (
mir-backend-rust). - A mechanical footgun of this library (extractor order,
StatevsExtension,FromRef,IntoResponse, Tower layer ordering)? → here. - A different Rust framework (Actix-web, Warp) → its own
mir-backend-rust-module. Never widen this one.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: anantbhandarkar
- Source: anantbhandarkar/make-it-right
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.