ワンクリックで
custom-middleware
Axum Tower 커스텀 미들웨어 작성 패턴 - from_fn, from_fn_with_state, 요청/응답 가로채기, 실전 예시
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Axum Tower 커스텀 미들웨어 작성 패턴 - from_fn, from_fn_with_state, 요청/응답 가로채기, 실전 예시
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Spring Security 5.5.x + jjwt 0.10.7 레거시 JWT 인증 - WebSecurityConfigurerAdapter, OncePerRequestFilter, javax.servlet 환경
Spring Boot 3.x + Spring Security 6.x + jjwt 0.12.x 기반 모던 JWT 인증 패턴. SecurityFilterChain Bean, 람다 DSL, jakarta.servlet, Virtual Threads 적용
Unity 6 LTS 2D 모바일 게임용 uGUI 시스템 전문 스킬. Canvas/RectTransform/TextMeshPro, 모바일 UI 패턴(팝업·무한 스크롤·광고·IAP), 성능 최적화, UI Toolkit과의 선택 기준 포함.
아크라시아(akrasia, 자제력 없음) 학술 논쟁의 핵심 구도와 주요 연구자·문헌을 빠르게 파악할 수 있는 도메인 지식 스킬. 도덕윤리교육 전공 대학원생(석/박사)이 학위논문·KCI 투고·세미나 준비 시 고대–현대–한국 학계–도덕심리학 흐름을 한 번에 짚도록 구성. <example>사용자: "아리스토텔레스의 propeteia와 astheneia 구분을 인용하려는데 출처를 알려줘"</example> <example>사용자: "데이비슨이 의지박약을 어떻게 가능하다고 봤는지 핵심 논증을 정리해줘"</example> <example>사용자: "한국 도덕교육 학계에서 아크라시아 다룬 논문 있어?"</example>
아리스토텔레스 『니코마코스 윤리학』에서 akrasia(자제력없음)와 akolasia(무절제)의 5축 차이를 정밀하게 정리한 학위논문 자료 스킬. NE VII.4 1147b20-1148b14, VII.8 1150b29-1151a28, III.10-12 1117b23-1119b18 절별 분해와 표준 학자 해석(Bostock, Broadie-Rowe, Pakaluk, Hursthouse, Charles 등)을 포함. 도덕교육 적용을 위한 두 상태 차이의 함의 및 한국어 번역어 처리 권장안 제공. <example>사용자: "akrates와 akolastos를 prohairesis 측면에서 어떻게 구분해야 하나요?"</example> <example>사용자: "NE VII.4의 ἁπλῶς akrasia가 akolasia와 어떻게 갈라지는지 절별 분해해주세요"</example> <example>사용자: "Hursthouse의 연속체 모델을 도덕교육 적용 절에서 어떻게 활용할 수 있나요?"</example>
한국 위기 대응 자원(자살·자해·정신건강·여성·청소년·노인·다문화) 핫라인과 앱·챗봇 안전 가드 응답 패턴 종합. 꿈 해몽·정신건강 앱 등 자가 진단/감정 콘텐츠 도메인에서 위험 신호 포착 시 안전한 자원 안내 문구를 작성할 때 참조. <example>사용자: "꿈 해몽 앱에 위기 안내 문구를 어떻게 넣을까?"</example> <example>사용자: "한국에서 자살예방 핫라인 번호가 어떻게 바뀌었지?"</example> <example>사용자: "정신건강 챗봇 안전 가드 응답 템플릿을 짜줘"</example>
| name | custom-middleware |
| description | Axum Tower 커스텀 미들웨어 작성 패턴 - from_fn, from_fn_with_state, 요청/응답 가로채기, 실전 예시 |
소스: https://docs.rs/axum/0.8.1/axum/middleware/index.html | https://docs.rs/axum/0.8.1/axum/middleware/fn.from_fn.html | https://docs.rs/axum/0.8.1/axum/middleware/fn.from_fn_with_state.html 검증일: 2026-06-20
주의: axum 0.8.x 기준. 0.6 이하에서는
Next<B>제네릭 파라미터가 필요했으나, 0.7부터Next는 제네릭 없이 사용한다. (DISPUTED 수정: 변경 기점이 0.8이 아닌 0.7임. axum 0.7 공식 발표 및 Discussion #2488에서 확인)
주의: 미들웨어 함수의
Request타입은 반드시axum::extract::Request를 사용해야 한다.axum::http::Request는 제네릭(Request<T>)이므로 그대로 쓰면 컴파일 에러가 발생한다.
| 상황 | 선택 |
|---|---|
| CORS, 압축, 트레이싱, 타임아웃 등 범용 기능 | tower-http 기성 미들웨어 |
| 비즈니스 로직이 포함된 인증/인가 | 커스텀 미들웨어 (from_fn) |
| 앱 State 접근이 필요한 미들웨어 | from_fn_with_state |
| 복잡한 상태 머신, 커넥션 풀링 등 | Tower Service 트레이트 직접 구현 |
대부분의 경우 from_fn으로 충분하다. Service 트레이트 직접 구현은 거의 필요하지 않다.
async fn을 미들웨어로 변환한다. 함수 시그니처: Request와 Next를 받고 impl IntoResponse를 반환.
use axum::{
extract::Request,
middleware::{self, Next},
response::Response,
Router,
routing::get,
};
async fn my_middleware(
request: Request,
next: Next,
) -> Response {
// 요청 전 처리
let response = next.run(request).await;
// 응답 후 처리
response
}
let app = Router::new()
.route("/", get(handler))
.layer(middleware::from_fn(my_middleware));
next.run(request)은 반드시 한 번만 호출한다.Next는 Clone이 아니므로 복수 호출은 컴파일 에러가 된다.미들웨어 함수에서 앱 State에 접근해야 할 때 사용한다.
use axum::{
extract::{Request, State},
http::StatusCode,
middleware::Next,
response::Response,
};
#[derive(Clone)]
struct AppState {
api_keys: Vec<String>,
}
async fn api_key_middleware(
State(state): State<AppState>,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let key = request
.headers()
.get("x-api-key")
.and_then(|v| v.to_str().ok());
match key {
Some(k) if state.api_keys.contains(&k.to_string()) => {
Ok(next.run(request).await)
}
_ => Err(StatusCode::UNAUTHORIZED),
}
}
let state = AppState {
api_keys: vec!["secret-key-1".into()],
};
let app = Router::new()
.route("/api/data", get(data_handler))
.route_layer(middleware::from_fn_with_state(
state.clone(),
api_key_middleware,
))
.with_state(state);
State 추출자는 반드시 Request, Next보다 앞에 와야 한다.
// 올바른 순서
async fn mw(
State(state): State<AppState>, // 1. State (선택)
request: Request, // 2. Request (필수)
next: Next, // 3. Next (필수)
) -> Response { ... }
State 외에 다른 추출자도 Request 앞에 배치할 수 있다.
async fn mw(
State(state): State<AppState>,
headers: HeaderMap, // 추가 추출자
request: Request,
next: Next,
) -> Response { ... }
이 섹션의 예시들은 아래 공통 import를 전제한다.
use axum::{
extract::Request,
http::StatusCode,
middleware::Next,
response::Response,
};
async fn log_request(
request: Request,
next: Next,
) -> Response {
let method = request.method().clone();
let uri = request.uri().clone();
let user_agent = request
.headers()
.get("user-agent")
.and_then(|v| v.to_str().ok())
.unwrap_or("unknown")
.to_string();
tracing::info!(%method, %uri, %user_agent, "incoming request");
next.run(request).await
}
본문을 읽으면 소비되므로, 읽은 후 새 Request로 재구성해야 한다.
use axum::body::Body;
use axum::extract::Request;
use http_body_util::BodyExt;
async fn log_body(
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let (parts, body) = request.into_parts();
// 본문을 bytes로 수집
let bytes = body
.collect()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.to_bytes();
tracing::info!(body = %String::from_utf8_lossy(&bytes), "request body");
// 새 Request로 재구성 (axum::extract::Request = http::Request<Body>의 타입 alias)
let request = Request::from_parts(parts, Body::from(bytes));
Ok(next.run(request).await)
}
주의: 요청 본문을 메모리에 전부 로드하므로 대용량 업로드에는 부적합하다.
RequestBodyLimitLayer와 함께 사용을 권장한다.
next.run()을 호출하지 않으면 요청이 핸들러에 도달하지 않는다.
async fn reject_if_missing_header(
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
if request.headers().get("x-required-header").is_none() {
return Err(StatusCode::BAD_REQUEST);
}
Ok(next.run(request).await)
}
이 섹션의 예시들도 axum::extract::Request, middleware::Next, response::Response import를 전제한다.
async fn add_response_header(
request: Request,
next: Next,
) -> Response {
let mut response = next.run(request).await;
response.headers_mut().insert(
"x-request-id",
uuid::Uuid::new_v4().to_string().parse().unwrap(),
);
response
}
async fn log_response(
request: Request,
next: Next,
) -> Response {
let method = request.method().clone();
let uri = request.uri().clone();
let start = std::time::Instant::now();
let response = next.run(request).await;
let latency = start.elapsed();
let status = response.status();
if status.is_server_error() {
tracing::error!(%method, %uri, %status, ?latency, "server error");
} else {
tracing::info!(%method, %uri, %status, ?latency, "response");
}
response
}
요청/응답 양쪽을 모두 로깅하는 통합 미들웨어.
use axum::{
extract::Request,
http::StatusCode,
middleware::Next,
response::Response,
};
use std::time::Instant;
async fn request_logging_middleware(
request: Request,
next: Next,
) -> Response {
let method = request.method().clone();
let uri = request.uri().clone();
let start = Instant::now();
tracing::info!(%method, %uri, "-> request");
let response = next.run(request).await;
let latency = start.elapsed();
let status = response.status();
tracing::info!(%method, %uri, %status, latency_ms = %latency.as_millis(), "<- response");
response
}
적용:
let app = Router::new()
.route("/api/users", get(list_users))
.layer(middleware::from_fn(request_logging_middleware));
DB나 설정에서 API 키를 검증하는 패턴.
use axum::{
extract::{Request, State},
http::StatusCode,
middleware::Next,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
#[derive(Clone)]
struct AppState {
valid_api_keys: Vec<String>,
}
async fn validate_api_key(
State(state): State<AppState>,
request: Request,
next: Next,
) -> Result<Response, Response> {
let api_key = request
.headers()
.get("x-api-key")
.and_then(|v| v.to_str().ok());
match api_key {
Some(key) if state.valid_api_keys.contains(&key.to_string()) => {
Ok(next.run(request).await)
}
Some(_) => Err((
StatusCode::FORBIDDEN,
Json(json!({"error": "Invalid API key"})),
).into_response()),
None => Err((
StatusCode::UNAUTHORIZED,
Json(json!({"error": "Missing x-api-key header"})),
).into_response()),
}
}
적용:
let state = AppState {
valid_api_keys: vec!["key-abc-123".into(), "key-def-456".into()],
};
let app = Router::new()
.route("/api/data", get(data_handler))
.route_layer(middleware::from_fn_with_state(
state.clone(),
validate_api_key,
))
.route("/health", get(health_handler)) // 미들웨어 미적용
.with_state(state);
route_layer를 사용하면 그 위에 선언된 라우트에만 미들웨어가 적용된다./health는 미들웨어를 통과하지 않는다.
미들웨어에서 추출한 데이터를 핸들러에 전달할 때 request.extensions_mut()를 사용한다.
use axum::{
extract::Request,
http::StatusCode,
middleware::Next,
response::Response,
};
#[derive(Clone)]
struct CurrentUser {
id: u64,
name: String,
}
async fn auth_middleware(
mut request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let token = request
.headers()
.get("authorization")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
let user = verify_token(token)
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
// Extensions에 삽입
request.extensions_mut().insert(user);
Ok(next.run(request).await)
}
// 핸들러에서 Extension으로 추출
use axum::Extension;
async fn protected_handler(
Extension(user): Extension<CurrentUser>,
) -> String {
format!("Hello, {}", user.name)
}
let app = Router::new()
.route("/protected", get(protected_handler))
.route("/public", get(public_handler))
.route_layer(middleware::from_fn(auth_mw)) // /protected, /public에 적용
.route("/health", get(health_handler)); // auth_mw 미적용
| 메서드 | 적용 범위 | 매칭 안 된 요청 |
|---|---|---|
.layer() | 모든 요청 (fallback 포함) | 미들웨어 통과 |
.route_layer() | 매칭된 라우트만 | 미들웨어 통과하지 않음 |
인증 미들웨어는 .route_layer()를 사용해야 404 응답이 인증 에러로 바뀌는 문제를 방지할 수 있다.
.layer() 호출 순서와 실행 순서는 역순이다.
let app = Router::new()
.route("/", get(handler))
.layer(middleware::from_fn(mw_a)) // 2번째 실행
.layer(middleware::from_fn(mw_b)); // 1번째 실행
// 요청 흐름: mw_b -> mw_a -> handler -> mw_a -> mw_b
직관적 순서를 원하면 tower::ServiceBuilder를 사용한다:
use tower::ServiceBuilder;
let app = Router::new()
.route("/", get(handler))
.layer(
ServiceBuilder::new()
.layer(middleware::from_fn(mw_a)) // 1번째 실행
.layer(middleware::from_fn(mw_b)), // 2번째 실행
);