Docs: - .env.example: documents all environment variables with defaults - CONTRIBUTING.md: dev setup, running tests, code style, design constraints - MODDING.md: full schema reference for cards, packs, objectives, and SBCs - .gitea/workflows/ci.yml: Gitea Actions CI (fmt check, clippy, build, test) Middleware (#50-52): - Body limit (256 KB) via DefaultBodyLimit on all routes (#50) - Concurrency limit (256 concurrent requests) via semaphore middleware; returns 429 Too Many Requests when at capacity (#51) - Correlation IDs via tower_http request_id layers; sets x-request-id UUID on every request and propagates it to the response headers (#52) Layer order (outermost → innermost): CorsLayer → DefaultBodyLimit → concurrency limit → SetRequestId → TraceLayer → PropagateRequestId → routes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+14
-1
@@ -1,3 +1,4 @@
|
||||
use crate::middleware::{limit_concurrency, MakeRequestUuid};
|
||||
use crate::{
|
||||
config::Config,
|
||||
db::Pool,
|
||||
@@ -12,11 +13,17 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
use axum::{
|
||||
extract::DefaultBodyLimit,
|
||||
middleware,
|
||||
routing::{delete, get, post, put},
|
||||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tower_http::{cors::CorsLayer, trace::TraceLayer};
|
||||
use tower_http::{
|
||||
cors::CorsLayer,
|
||||
request_id::{PropagateRequestIdLayer, SetRequestIdLayer},
|
||||
trace::TraceLayer,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
@@ -141,7 +148,13 @@ pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
|
||||
.route("/settings", get(routes::settings::get_settings))
|
||||
.route("/settings", put(routes::settings::put_settings))
|
||||
.route("/draft/squad", get(routes::draft::get_draft_squad))
|
||||
// Layers run outermost-first (last added = outermost).
|
||||
// CORS → body limit → concurrency limit → request ID → trace
|
||||
.layer(PropagateRequestIdLayer::x_request_id())
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
|
||||
.layer(middleware::from_fn(limit_concurrency))
|
||||
.layer(DefaultBodyLimit::max(256 * 1024))
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod app;
|
||||
pub mod config;
|
||||
pub mod db;
|
||||
pub mod error;
|
||||
pub mod middleware;
|
||||
pub mod modding;
|
||||
pub mod models;
|
||||
pub mod routes;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
use axum::{extract::Request, http::StatusCode, middleware::Next, response::Response};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use tokio::sync::Semaphore;
|
||||
use tower_http::request_id::{MakeRequestId, RequestId};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Maximum concurrent in-flight requests. Responds 429 when at capacity.
|
||||
const MAX_CONCURRENT: usize = 256;
|
||||
|
||||
static SEMAPHORE: LazyLock<Arc<Semaphore>> =
|
||||
LazyLock::new(|| Arc::new(Semaphore::new(MAX_CONCURRENT)));
|
||||
|
||||
/// Axum middleware that limits concurrent in-flight requests.
|
||||
/// Returns 429 Too Many Requests when the limit is reached.
|
||||
pub async fn limit_concurrency(req: Request, next: Next) -> Result<Response, StatusCode> {
|
||||
let Ok(_permit) = SEMAPHORE.try_acquire() else {
|
||||
return Err(StatusCode::TOO_MANY_REQUESTS);
|
||||
};
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
|
||||
/// Generates a UUID v4 for each request and attaches it as `x-request-id`.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MakeRequestUuid;
|
||||
|
||||
impl MakeRequestId for MakeRequestUuid {
|
||||
fn make_request_id<B>(&mut self, _: &http::Request<B>) -> Option<RequestId> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let header_val = id.parse().ok()?;
|
||||
Some(RequestId::new(header_val))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user