Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions backend/api/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from backend.api.utils import (
BODY_TOO_LARGE,
body_too_large_error,
build_cache_refresh_payload,
error,
get_database,
Expand Down Expand Up @@ -217,7 +218,9 @@ async def get_plex_cache(
}
}
},
}
},
400: {"description": "Malformed JSON, or not an object of string lists"},
413: {"description": "Request body too large"},
},
)
async def refresh_cache(
Expand All @@ -229,11 +232,7 @@ async def refresh_cache(
try:
payload = await read_request_json(request)
if payload is BODY_TOO_LARGE:
return error(
"Request body too large",
code="BODY_TOO_LARGE",
status_code=413,
)
return body_too_large_error()

logger.debug(f"Serving POST /api/cache/refresh with payload: {payload}")

Expand Down
21 changes: 19 additions & 2 deletions backend/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@
from fastapi import APIRouter, Depends, Query, Request
from fastapi.responses import JSONResponse

from backend.api.utils import error, get_logger, ok
from backend.api.utils import (
BODY_TOO_LARGE,
body_too_large_error,
error,
get_logger,
ok,
read_request_json,
)
from backend.util.config import (
ConfigError,
ChubConfig,
Expand Down Expand Up @@ -171,7 +178,17 @@ async def update_config(
Confirmation of update with count of changes applied
"""
try:
incoming = await request.json()
incoming = await read_request_json(request)
if incoming is BODY_TOO_LARGE:
return body_too_large_error()
# An unusable body must not reach the merge: it would re-save the
# current config unchanged rather than fail the request.
if not isinstance(incoming, dict) or not incoming:
return error(
"Configuration validation failed",
"CONFIG_VALIDATION_ERROR",
status_code=400,
)
logger.debug("Serving POST /api/config")

current_config = load_config()
Expand Down
71 changes: 42 additions & 29 deletions backend/api/media_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@

from backend.api.utils import (
BODY_TOO_LARGE,
body_too_large_error,
build_cache_refresh_payload,
error,
get_database,
get_logger,
ok,
read_json_object,
read_request_json,
)
from backend.util.arr import create_arr_client
Expand Down Expand Up @@ -538,7 +540,9 @@ def _not_excluded(dup):
}
}
},
}
},
400: {"description": "Malformed JSON, or not an object of string lists"},
413: {"description": "Request body too large"},
},
)
async def refresh_media(
Expand All @@ -550,11 +554,7 @@ async def refresh_media(
try:
payload = await read_request_json(request)
if payload is BODY_TOO_LARGE:
return error(
"Request body too large",
code="BODY_TOO_LARGE",
status_code=413,
)
return body_too_large_error()

logger.debug(f"Serving POST /api/media/refresh with payload: {payload}")

Expand Down Expand Up @@ -627,11 +627,9 @@ async def export_media(
Exported data with format identifier and item count
"""
try:
payload = (
await request.json()
if request.headers.get("content-type") == "application/json"
else {}
)
payload = await read_json_object(request)
if payload is BODY_TOO_LARGE:
return body_too_large_error()
export_format = payload.get("format", "json")
fields = payload.get("fields")
logger.debug(f"Serving POST /api/media/export format={export_format}")
Expand Down Expand Up @@ -774,7 +772,9 @@ async def create_collection(
Created or updated collection record
"""
try:
payload = await request.json()
payload = await read_json_object(request)
if payload is BODY_TOO_LARGE:
return body_too_large_error()
logger.debug(f"Serving POST /api/media/collections with payload: {payload}")

title = payload.get("title")
Expand Down Expand Up @@ -866,7 +866,17 @@ async def update_collection(
Updated collection record
"""
try:
payload = await request.json()
payload = await read_request_json(request)
if payload is BODY_TOO_LARGE:
return body_too_large_error()
# An unusable body used to raise into the 500 handler; keep that
# response — falling through would upsert the record unchanged.
if not isinstance(payload, dict) or not payload:
return error(
"Error updating collection",
code="COLLECTION_UPDATE_ERROR",
status_code=500,
)
logger.debug(
f"Serving PUT /api/media/collections/{collection_id} with payload: {payload}"
)
Expand Down Expand Up @@ -967,9 +977,10 @@ async def resolve_duplicates(
"""
logger.debug(f"Serving POST /api/media/duplicates/{group_id}/resolve")

try:
body = await request.json()
except Exception:
body = await read_request_json(request)
if body is BODY_TOO_LARGE:
return body_too_large_error()
if not isinstance(body, dict):
return error("Invalid request body", code="INVALID_BODY", status_code=400)

keep_id = body.get("keepId")
Expand Down Expand Up @@ -1115,9 +1126,10 @@ async def bulk_delete_media(
"""Delete every id in `ids` from its ARR instance (optional file delete)
and the local cache. Body: { ids: [int], deleteFiles?: bool,
addImportExclusion?: bool }."""
try:
body = await request.json()
except Exception:
body = await read_request_json(request)
if body is BODY_TOO_LARGE:
return body_too_large_error()
if not isinstance(body, dict):
return error("Invalid request body", code="INVALID_BODY", status_code=400)

ids = body.get("ids", [])
Expand Down Expand Up @@ -1736,7 +1748,9 @@ async def update_media_metadata(
List of field names that were updated
"""
try:
payload = await request.json()
payload = await read_json_object(request)
if payload is BODY_TOO_LARGE:
return body_too_large_error()
logger.debug(f"Serving PUT /api/media/{media_id}/metadata")

item = db.media.get_by_id(media_id)
Expand Down Expand Up @@ -1956,14 +1970,11 @@ async def delete_media_item(
status_code=404,
)

# Parse optional JSON body for deleteFiles flag
delete_files = False
try:
if request.headers.get("content-type") == "application/json":
body = await request.json()
delete_files = body.get("deleteFiles", False)
except Exception:
pass # optional body; malformed JSON just leaves delete_files=False
# Optional body; a missing or malformed one leaves delete_files=False
body = await read_json_object(request)
if body is BODY_TOO_LARGE:
return body_too_large_error()
delete_files = body.get("deleteFiles", False)

# If deleteFiles requested, remove from ARR first. The connect probe +
# delete request are blocking, so run them off the event loop.
Expand Down Expand Up @@ -2047,7 +2058,9 @@ async def generate_collection_from_tag(
) -> JSONResponse:
"""Build a poster_collection from every media row carrying `tag`."""
try:
payload = await request.json()
payload = await read_json_object(request)
if payload is BODY_TOO_LARGE:
return body_too_large_error()
tag = (payload.get("tag") or "").strip()
if not tag:
return error("tag required", code="TAG_REQUIRED", status_code=400)
Expand Down
34 changes: 31 additions & 3 deletions backend/api/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@
from starlette.concurrency import run_in_threadpool
from pydantic import BaseModel

from backend.api.utils import error, get_database, get_logger, ok
from backend.api.utils import (
BODY_TOO_LARGE,
body_too_large_error,
error,
get_database,
get_logger,
ok,
read_request_json,
)
from backend.util.config import ConfigError
from backend.util.database import ChubDB

Expand Down Expand Up @@ -906,7 +914,17 @@ async def update_module_config(
Confirmation of the configuration update
"""
try:
payload = await request.json()
payload = await read_request_json(request)
if payload is BODY_TOO_LARGE:
return body_too_large_error()
# An unusable body must not reach the merge: it would blank the
# section back to its defaults instead of failing the request.
if not isinstance(payload, dict) or not payload:
return error(
"Config validation failed",
code="CONFIG_VALIDATION_ERROR",
status_code=400,
)
logger.debug(f"Serving PUT /api/modules/{name}/config")
from backend.util.config import (
ChubConfig,
Expand Down Expand Up @@ -1010,7 +1028,17 @@ async def toggle_module(
Confirmation with the new enabled state
"""
try:
payload = await request.json()
payload = await read_request_json(request)
if payload is BODY_TOO_LARGE:
return body_too_large_error()
# An unusable body used to raise into the 500 handler; keep that
# response — falling through would read as `enabled: false`.
if not isinstance(payload, dict) or not payload:
return error(
"Error toggling module",
code="MODULE_TOGGLE_ERROR",
status_code=500,
)
enabled = payload.get("enabled")
logger.debug(f"Serving PATCH /api/modules/{name} enabled={enabled}")
from backend.util.config import load_config, save_config
Expand Down
Loading
Loading