fix(rust): read the request body before answering

A 403 or unknown-route 404 sent before the POST body was read raced
Carloku's edge and came back as a 502 about a third of the time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 8b9a728d271d155cd35510903d28ba0270b3c9d1 parent 4aa94e2
1 file changed, +20 −6
  • apps/rust/src/app.rs +20 −6
diff --git a/apps/rust/src/app.rs b/apps/rust/src/app.rs
index ef3b4f8..f4a4d66 100644
--- a/apps/rust/src/app.rs
+++ b/apps/rust/src/app.rs
@@ -58,18 +58,32 @@ pub fn router(state: AppState) -> Router {
.with_state(state)
}
-/// Refuses a POST whose `Origin` is present and isn't ours. No `Origin`
-/// header is allowed: the contract has no CSRF token, and SameSite=Lax
-/// already keeps the cookie off cross-site POSTs.
+/// The largest request body we read: a 10,000-character title is ~90 KB
+/// form-encoded at worst.
+const BODY_LIMIT: usize = 1 << 20;
+
+/// Reads the whole request body before anything answers, then refuses a
+/// POST whose `Origin` is present and isn't ours.
+///
+/// Reading first matters behind Carloku's edge: a response sent while the
+/// body is still unread (a 403 here, a 404 for an unknown route) makes
+/// hyper drop the connection, and the edge reports that as a 502.
+///
+/// No `Origin` header is allowed: the contract has no CSRF token, and
+/// SameSite=Lax already keeps the cookie off cross-site POSTs.
async fn same_origin(State(s): State<AppState>, req: Request, next: Next) -> Response {
- if req.method() == Method::POST {
- if let Some(origin) = req.headers().get(header::ORIGIN) {
+ let (parts, body) = req.into_parts();
+ let Ok(bytes) = axum::body::to_bytes(body, BODY_LIMIT).await else {
+ return StatusCode::PAYLOAD_TOO_LARGE.into_response();
+ };
+ if parts.method == Method::POST {
+ if let Some(origin) = parts.headers.get(header::ORIGIN) {
if origin.as_bytes() != s.origin.as_bytes() {
return StatusCode::FORBIDDEN.into_response();
}
}
}
- next.run(req).await
+ next.run(Request::from_parts(parts, axum::body::Body::from(bytes))).await
}
// ---- the visitor's list ----------------------------------------------