fix: drain multipart body before returning early upload errors

When the rate limit, ban, or event-lock check fired before the multipart
body was consumed, the HTTP keep-alive connection was left in a bad state.
The next request from the Vite proxy then received a connection reset,
causing cascading "Netzwerkfehler" / empty-body 500 responses.

Fix: call drain_multipart() before every early return in the upload
handler so the connection remains clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-04-03 18:07:27 +02:00
parent ee7c703c13
commit c58216268f

View File

@@ -24,6 +24,7 @@ pub async fn upload(
.rate_limiter .rate_limiter
.check(format!("upload:{}", auth.user_id), upload_rate, Duration::from_secs(3600)) .check(format!("upload:{}", auth.user_id), upload_rate, Duration::from_secs(3600))
{ {
drain_multipart(multipart).await;
return Err(AppError::TooManyRequests( return Err(AppError::TooManyRequests(
"Du hast dein Upload-Limit für diese Stunde erreicht.".into(), "Du hast dein Upload-Limit für diese Stunde erreicht.".into(),
)); ));
@@ -34,6 +35,7 @@ pub async fn upload(
.await? .await?
.ok_or_else(|| AppError::NotFound("Benutzer nicht gefunden.".into()))?; .ok_or_else(|| AppError::NotFound("Benutzer nicht gefunden.".into()))?;
if user.is_banned { if user.is_banned {
drain_multipart(multipart).await;
return Err(AppError::Forbidden("Du bist gesperrt.".into())); return Err(AppError::Forbidden("Du bist gesperrt.".into()));
} }
@@ -42,6 +44,7 @@ pub async fn upload(
.await? .await?
.ok_or_else(|| AppError::NotFound("Event nicht gefunden.".into()))?; .ok_or_else(|| AppError::NotFound("Event nicht gefunden.".into()))?;
if event.uploads_locked_at.is_some() { if event.uploads_locked_at.is_some() {
drain_multipart(multipart).await;
return Err(AppError::Forbidden("Uploads sind gesperrt.".into())); return Err(AppError::Forbidden("Uploads sind gesperrt.".into()));
} }
@@ -239,6 +242,15 @@ pub async fn delete_upload(
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
/// Drain a multipart body so the HTTP connection stays clean when returning an early error.
/// Without draining, the client may still be sending the body after we've sent our response,
/// which can corrupt the keep-alive connection for subsequent requests.
async fn drain_multipart(mut mp: Multipart) {
while let Ok(Some(mut field)) = mp.next_field().await {
while field.chunk().await.ok().flatten().is_some() {}
}
}
async fn get_config_i64(pool: &sqlx::PgPool, key: &str, default: i64) -> i64 { async fn get_config_i64(pool: &sqlx::PgPool, key: &str, default: i64) -> i64 {
let row: Option<(String,)> = let row: Option<(String,)> =
sqlx::query_as("SELECT value FROM config WHERE key = $1") sqlx::query_as("SELECT value FROM config WHERE key = $1")