commit e0e3d681a922eb0b6d85130a144e150eae226f34
parent e09ad3174c4e02ee4d203556cf4b90fc4821acb6
Author: Silas Brack <silasbrack@gmail.com>
Date: Tue, 9 Jun 2026 20:07:43 +0200
feat: add /api/* reverse proxy for local development
Proxy TrailBase API requests through the Axum app so SSE subscriptions
and other /api/* calls work locally without a separate reverse proxy.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 56 insertions(+), 2 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1663,6 +1663,7 @@ dependencies = [
"bytes",
"encoding_rs",
"futures-core",
+ "futures-util",
"h2",
"http",
"http-body",
@@ -1684,12 +1685,14 @@ dependencies = [
"sync_wrapper",
"tokio",
"tokio-native-tls",
+ "tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
+ "wasm-streams 0.4.2",
"web-sys",
]
@@ -1730,7 +1733,7 @@ dependencies = [
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
- "wasm-streams",
+ "wasm-streams 0.5.0",
"web-sys",
]
@@ -2604,6 +2607,19 @@ dependencies = [
[[package]]
name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-streams"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
diff --git a/Cargo.toml b/Cargo.toml
@@ -13,6 +13,6 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
tracing = "0.1"
-reqwest = { version = "0.12", features = ["json"] }
+reqwest = { version = "0.12", features = ["json", "stream"] }
trailbase-client = "0.8"
url = "2"
diff --git a/src/routes.rs b/src/routes.rs
@@ -1,6 +1,7 @@
use axum::{
Router,
body::Body,
+ extract::{Request, State},
http::{StatusCode, header},
response::{IntoResponse, Response},
routing::{get, post},
@@ -70,6 +71,7 @@ pub fn create_router(state: AppState) -> Router {
.route("/story/{id}/edit", post(handlers::edit_story))
// Comments
+ .route("/story/{id}/comments", get(handlers::story_comments_fragment))
.route("/story/{id}/comment", post(handlers::create_comment))
.route("/comment/{id}/reply", get(handlers::show_reply_form))
.route("/comment/{id}/reply", post(handlers::submit_reply))
@@ -101,9 +103,45 @@ pub fn create_router(state: AppState) -> Router {
.route("/resend-verification", get(handlers::show_resend_verification))
.route("/resend-verification", post(handlers::resend_verification))
+ // Reverse proxy to TrailBase for /api/* (SSE subscriptions, etc.)
+ .route("/api/{*path}", axum::routing::any(proxy_to_trailbase))
+
// Static files
.route("/static/style.css", get(serve_style))
.route("/static/datastar.js", get(serve_datastar))
.route("/static/{*path}", get(static_404))
.with_state(state)
}
+
+async fn proxy_to_trailbase(
+ State(state): State<AppState>,
+ request: Request,
+) -> Response {
+ let path = request.uri().path_and_query()
+ .map(|pq| pq.as_str())
+ .unwrap_or(request.uri().path());
+
+ let url = format!("{}{}", state.trailbase_url.trim_end_matches('/'), path);
+
+ let response = match state.http_client.get(&url).send().await {
+ Ok(resp) => resp,
+ Err(_) => {
+ return Response::builder()
+ .status(StatusCode::BAD_GATEWAY)
+ .body(Body::from("TrailBase unavailable"))
+ .unwrap();
+ }
+ };
+
+ let status = response.status();
+ let mut builder = Response::builder().status(status);
+
+ // Forward content-type and SSE-relevant headers
+ for (name, value) in response.headers() {
+ builder = builder.header(name, value);
+ }
+
+ // Stream the body through for SSE support
+ let body = Body::from_stream(response.bytes_stream());
+ builder.body(body).unwrap()
+}