fix: reject a malformed reenqueue body instead of running the full scope

Option<Json<T>> collapses a malformed body to None, so a bad reenqueue body
silently ran the default full "All" scope (audit). Parse raw bytes: empty =
default All, malformed = 422. Tested both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-13 21:37:16 +02:00
parent 4f085f228a
commit cc8ae2566f
6 changed files with 75 additions and 5 deletions

View File

@@ -328,10 +328,21 @@ fn ensure_enabled(state: &AppState) -> AppResult<()> {
async fn reenqueue(
State(state): State<AppState>,
admin: RequireAdmin,
body: Option<Json<ReenqueueBody>>,
raw: axum::body::Bytes,
) -> AppResult<Json<ReenqueueResponse>> {
ensure_enabled(&state)?;
let body = body.map(|b| b.0).unwrap_or_default();
// An ABSENT/empty body means the default "All" scope. A PRESENT body must be
// valid JSON — `Option<Json<T>>` would silently collapse a malformed body to
// None and run the full-library default the caller never intended, so parse
// the raw bytes ourselves and 422 on a real parse error.
let body: ReenqueueBody = if raw.iter().all(u8::is_ascii_whitespace) {
ReenqueueBody::default()
} else {
serde_json::from_slice(&raw).map_err(|e| AppError::ValidationFailed {
message: "reenqueue body is not valid JSON".into(),
details: json!({ "body": e.to_string() }),
})?
};
// Resolve the scope. manga_id and chapter_id are mutually exclusive;
// an unknown target is a 404 rather than a silent zero-enqueue.