One bounded HTTP client for every server-side fetch; uploads refused while streaming

Every outbound fetch - Gitea content at boot and on reload, the raw
asset proxy, the repo-info endpoint, the content-declared url resource
- built its own reqwest client with no timeout and read whole bodies
with .text(). `crate::http` is now the one client (5 s connect, 20 s
per request, 5 redirects) with capped body readers: 2 MB for a url
resource, 8 MB for a Gitea reply, 32 MB for a proxied asset; one byte
over is an error, never a truncated document.

The upload handler read a whole field into memory before comparing it
to the 20 MB cap, and axum's 2 MB default body limit had made that cap
unreachable anyway. It now reads chunk by chunk and refuses the moment
the cap is crossed; the /upload route raises the body limit to match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-22 16:51:15 +02:00
co-authored by Claude Fable 5.1
parent d7f60eaa31
commit c91bf59946
5 changed files with 99 additions and 18 deletions
+14 -6
View File
@@ -15,7 +15,7 @@ use serde::Serialize;
use crate::server::AppState;
const MAX_UPLOAD_BYTES: usize = 20 * 1024 * 1024;
pub const MAX_UPLOAD_BYTES: usize = 20 * 1024 * 1024;
#[derive(Clone)]
pub struct Garage {
@@ -84,12 +84,20 @@ pub async fn upload(
.unwrap_or("application/octet-stream")
.to_string();
let filename = field.file_name().unwrap_or("upload").to_string();
let bytes = field
.bytes()
// Chunk by chunk, refused the moment the cap is crossed: an
// oversized body is never buffered whole before being turned
// away. The route's body limit (main.rs) is the outer fence.
let mut bytes: Vec<u8> = Vec::new();
let mut field = field;
while let Some(chunk) = field
.chunk()
.await
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
if bytes.len() > MAX_UPLOAD_BYTES {
return Err((StatusCode::PAYLOAD_TOO_LARGE, "file too large".to_string()));
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?
{
if bytes.len() + chunk.len() > MAX_UPLOAD_BYTES {
return Err((StatusCode::PAYLOAD_TOO_LARGE, "file too large".to_string()));
}
bytes.extend_from_slice(&chunk);
}
let key = format!("{}-{filename}", uuid::Uuid::new_v4());