#HttpIngress 패턴 Cookbook
버전: 0.36.0 | 업데이트: 2026-04-03 | 적용 대상: ranvier-http 0.36+ | 카테고리: 쿡북
#개요
HttpIngress는 웹 서버가 아닌 Ingress Circuit Builder입니다. Axon 회로를
HTTP 라우트에 연결하고, Guard를 관리하며, 실제 요청 처리는 hyper에 위임합니다.
이 Cookbook에서는 라우팅, 바디 파싱, 정적 자산 제공의 주요 패턴을 다룹니다.
#1. `post()` vs `post_typed::()` 결정 트리
flowchart TD
Q{"라우트가 JSON 요청 바디를 받는가?"}
Q -->|아니오| P["post() 사용\nAxon‹(), Out, E, R›\n바디 무시; 회로는 ()를 받음"]
Q -->|예| PT["post_typed::‹T›() 사용\nAxon‹T, Out, E, R›\n바디가 자동으로 T로 역직렬화됨\nT: DeserializeOwned + Serialize + JsonSchema"]#`post()` -- 바디 없음
use ranvier_http::prelude::*;
let trigger_circuit = Axon::simple::<String>("trigger")
.then(start_job);
Ranvier::http()
.post("/api/jobs/start", trigger_circuit)
.run(())
.await?;#`post_typed::()` -- 타입 기반 JSON 바디
use ranvier_http::prelude::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct CreateOrder {
product_id: String,
quantity: u32,
shipping_address: String,
}
let order_circuit = Axon::typed::<CreateOrder, String>("create-order")
.then(validate_order)
.then(persist_order);
Ranvier::http()
.post_typed::<CreateOrder, _, _>("/api/orders", order_circuit)
.run(())
.await?;post_typed는 자동으로:
- 요청 바디를
T로 JSON 역직렬화 - 파싱 실패 시 400 Bad Request 반환
- OpenAPI 스키마 생성을 위해
JsonSchema기록
put_typed::<T>()와 patch_typed::<T>()에도 동일 패턴이 적용됩니다.
#`schemars` Feature 요구 사항
JsonSchema derive는 요청 구조체의 필드 타입에 따라 추가 schemars feature가 필요할 수 있습니다.
derive 컴파일이 실패하면 아래 표를 확인하세요:
| 필드 타입 | 필요 feature | Cargo.toml 예시 |
|---|---|---|
Uuid |
uuid1 |
schemars = { version = "1", features = ["derive", "uuid1"] } |
NaiveDate, DateTime<Utc> |
chrono04 |
schemars = { version = "1", features = ["derive", "chrono04"] } |
Decimal |
rust_decimal1 |
schemars = { version = "1", features = ["derive", "rust_decimal1"] } |
Url |
url2 |
schemars = { version = "1", features = ["derive", "url2"] } |
여러 feature를 조합할 수 있습니다: features = ["derive", "uuid1", "chrono04"].
#2. 경로 파라미터 추출
라우트 경로에 :param 구문을 사용합니다. 파라미터는 Bus의 PathParams를 통해
접근합니다.
use ranvier_http::prelude::*;
use ranvier_core::prelude::*;
let get_user = Axon::simple::<String>("get-user")
.then_fn("extract-id", |_input, _res, bus| async move {
let params = bus.require::<PathParams>();
let user_id = params.get("id").unwrap_or("unknown");
Outcome::next(format!("User: {}", user_id))
});
Ranvier::http()
.get("/api/users/:id", get_user)
.get("/api/users/:id/posts/:slug", get_user_post)
.run(())
.await?;다중 파라미터 지원: /api/orgs/:org_id/teams/:team_id.
#`PathParams::get()`은 `&str`를 반환
PathParams::get()은 Option<&String>이 아닌 Option<&str>를 반환합니다. 소유된
String이 필요하면 .cloned() 대신 .map(|s| s.to_string())를 사용하세요:
// 경로 파라미터를 소유된 String으로 읽기
let id: Option<String> = bus.get_cloned::<PathParams>().ok()
.and_then(|params| params.get("id").map(|s| s.to_string()));
// 경로 파라미터를 Uuid로 파싱
let uuid: Option<uuid::Uuid> = bus.get_cloned::<PathParams>().ok()
.and_then(|params| params.get("id").and_then(|s| s.parse().ok()));마이그레이션 참고:
HashMap<String, String>패턴에서 마이그레이션하는 경우,PathParams::get()이&String이 아닌&str를 반환하므로.cloned()를.map(|s| s.to_string())로 교체하세요.
#3. 요청 컨텍스트용 Bus 인젝터
bus_injector()를 사용하면 Guard나 회로 실행 전에 임의의 HTTP 요청 데이터를
Bus에 주입할 수 있습니다. 내장 Guard가 다루지 않는 데이터를 전달할 때 유용합니다.
use ranvier_http::prelude::*;
use ranvier_core::bus::Bus;
#[derive(Debug, Clone)]
struct UserAgent(String);
#[derive(Debug, Clone)]
struct AcceptLanguage(String);
Ranvier::http()
.bus_injector(|parts: &http::request::Parts, bus: &mut Bus| {
if let Some(ua) = parts.headers.get("user-agent") {
if let Ok(s) = ua.to_str() {
bus.insert(UserAgent(s.to_string()));
}
}
if let Some(lang) = parts.headers.get("accept-language") {
if let Ok(s) = lang.to_str() {
bus.insert(AcceptLanguage(s.to_string()));
}
}
})
.get("/api/data", data_circuit)
.run(())
.await?;회로 내에서 주입된 값을 읽습니다:
let circuit = Axon::simple::<String>("data")
.then_fn("use-context", |_input, _res, bus| async move {
let ua = bus.get_cloned::<UserAgent>()
.map(|u| u.0)
.unwrap_or_else(|_| "unknown".into());
Outcome::next(format!("Hello from {}", ua))
});#4. Guard + 라우트 통합
Guard와 라우트는 자연스럽게 구성됩니다. 글로벌 Guard는 모든 라우트에, 라우트별 Guard는 특정 엔드포인트에만 적용됩니다.
use ranvier_http::{guards, prelude::*};
use ranvier_guard::prelude::*;
Ranvier::http()
// 글로벌: 모든 요청에 적용
.guard(RequestIdGuard::new())
.guard(AccessLogGuard::new())
.guard(CorsGuard::<()>::permissive())
// 읽기 엔드포인트: 추가 Guard 없음
.get("/api/products", list_products)
.get("/api/products/:id", get_product)
// 쓰기 엔드포인트: 라우트별 Guard
.post_with_guards("/api/products", create_product, guards![
ContentTypeGuard::json(),
RequestSizeLimitGuard::max_2mb(),
])
.put_with_guards("/api/products/:id", update_product, guards![
ContentTypeGuard::json(),
])
.delete("/api/products/:id", delete_product)
.run(())
.await?;#5. `serve_dir()`로 정적 자산 제공
정적 파일 제공을 위해 디렉터리를 마운트합니다. MIME 타입 감지, 304 캐싱, 디렉터리 인덱스, 사전 압축 파일 등을 Ranvier가 자동으로 처리합니다.
serve_dir()는 API 라우트와 빌드된 프론트엔드를 한 프로세스에서 함께 제공할 때 가장 적합합니다. 순수 정적 호스팅이나 CDN 중심 자산 배포는 nginx, Caddy, object storage/CDN, 또는tower-http::ServeDir쪽이 더 적합합니다.
#기본 설정
use ranvier_http::prelude::*;
Ranvier::http()
.serve_dir("/static", "./public")
.get("/api/data", data_circuit)
.run(())
.await?;#SPA 폴백
Ranvier::http()
.serve_dir("/", "./dist")
.directory_index("index.html")
.spa_fallback("./dist/index.html")
.run(())
.await?;#프로덕션 정적 자산
Ranvier::http()
.serve_dir("/assets", "./build/assets")
.directory_index("index.html")
.static_cache_control("public, max-age=3600")
.immutable_cache() // 해시된 파일명에 max-age=31536000 적용
.serve_precompressed() // .br / .gz 파일 우선 확인
.enable_range_requests() // Range: bytes=X-Y 지원 (206 응답)
.get("/api/data", data_circuit)
.run(())
.await?;immutable_cache()는 콘텐츠 해시가 포함된 파일명(예: app.a1b2c3.js)을 감지하여
Cache-Control: public, max-age=31536000, immutable을 적용합니다.
serve_precompressed()는 원본 파일 제공 전에 .br와 .gz 사전 압축 파일을
확인합니다. 사전 압축 파일을 생성하는 빌드 도구와 함께 사용하면 효과적입니다.
enable_range_requests()는 부분 콘텐츠 응답(HTTP 206) 지원을 추가하며,
대용량 파일 다운로드와 미디어 스트리밍에 유용합니다.
#6. 헬스 엔드포인트
use ranvier_http::prelude::*;
Ranvier::http()
.health_endpoint("/health")
.readiness_liveness("/ready", "/live")
.health_check("database", |resources| async move {
// 헬스 체크 로직
Ok(())
})
.get("/api/data", data_circuit)
.run(())
.await?;기본 경로(/health/ready, /health/live) 사용:
Ranvier::http()
.readiness_liveness_default()
.run(())
.await?;#7. WebSocket 라우트
use ranvier_http::prelude::*;
Ranvier::http()
.ws("/ws/chat", |mut conn, _resources, _bus| async move {
conn.send(WebSocketEvent::text("Welcome!")).await.ok();
while let Ok(Some(msg)) = conn.next_json::<serde_json::Value>().await {
let echo = WebSocketEvent::json(&msg).unwrap();
if conn.send(echo).await.is_err() {
break;
}
}
})
.run(())
.await?;#8. 오류 핸들러
라우트에 커스텀 오류-응답 변환기를 등록합니다:
use ranvier_http::prelude::*;
Ranvier::http()
.post_with_error("/api/orders", order_circuit, |error| {
match error.to_string().as_str() {
e if e.contains("duplicate") => (409, "Conflict: 중복 주문"),
e if e.contains("invalid") => (400, "Bad Request: 유효하지 않은 입력"),
_ => (500, "Internal Server Error"),
}
})
.run(())
.await?;#관련 문서
- Guard 패턴 Cookbook -- Guard 구성 패턴
- Bus 접근 패턴 -- Bus 읽기/쓰기 결정 가이드
- 배포 가이드 -- 프로덕션 배포 설정