From 8a2876fe869368dda2fa71b538771c2092aa5805 Mon Sep 17 00:00:00 2001 From: anti Date: Mon, 20 Apr 2026 15:25:02 -0400 Subject: [PATCH] fix(api): document missing HTTP status codes on router endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schemathesis was failing CI on routes that returned status codes not declared in their OpenAPI responses= dicts. Adds the missing codes across swarm_updates, swarm_mgmt, swarm, fleet and attackers routers. Also adds 400 to every POST/PUT/PATCH that accepts a JSON body — Starlette returns 400 on malformed/non-UTF8 bodies before FastAPI's 422 validation runs, which schemathesis fuzzing trips every time. No handler logic changed. --- decnet/web/router/attackers/api_get_attacker_commands.py | 1 + decnet/web/router/fleet/api_mutate_decky.py | 7 ++++++- decnet/web/router/swarm/api_enroll_host.py | 6 +++++- decnet/web/router/swarm/api_heartbeat.py | 2 ++ decnet/web/router/swarm/api_teardown_swarm.py | 6 +++++- decnet/web/router/swarm_mgmt/api_decommission_host.py | 6 ++++++ decnet/web/router/swarm_mgmt/api_enroll_bundle.py | 7 +++++++ decnet/web/router/swarm_mgmt/api_teardown_host.py | 7 +++++++ decnet/web/router/swarm_updates/api_list_host_releases.py | 4 ++++ decnet/web/router/swarm_updates/api_push_update.py | 7 +++++++ decnet/web/router/swarm_updates/api_push_update_self.py | 7 +++++++ decnet/web/router/swarm_updates/api_rollback_host.py | 7 +++++++ 12 files changed, 64 insertions(+), 3 deletions(-) diff --git a/decnet/web/router/attackers/api_get_attacker_commands.py b/decnet/web/router/attackers/api_get_attacker_commands.py index c24cdb9..14d03eb 100644 --- a/decnet/web/router/attackers/api_get_attacker_commands.py +++ b/decnet/web/router/attackers/api_get_attacker_commands.py @@ -15,6 +15,7 @@ router = APIRouter() 401: {"description": "Could not validate credentials"}, 403: {"description": "Insufficient permissions"}, 404: {"description": "Attacker not found"}, + 422: {"description": "Query parameter validation error (limit/offset out of range or invalid)"}, }, ) @_traced("api.get_attacker_commands") diff --git a/decnet/web/router/fleet/api_mutate_decky.py b/decnet/web/router/fleet/api_mutate_decky.py index ea47be0..7f2e095 100644 --- a/decnet/web/router/fleet/api_mutate_decky.py +++ b/decnet/web/router/fleet/api_mutate_decky.py @@ -11,7 +11,12 @@ router = APIRouter() @router.post( "/deckies/{decky_name}/mutate", tags=["Fleet Management"], - responses={401: {"description": "Could not validate credentials"}, 403: {"description": "Insufficient permissions"}, 404: {"description": "Decky not found"}} + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, + 404: {"description": "Decky not found"}, + 422: {"description": "Path parameter validation error (decky_name must match ^[a-z0-9\\-]{1,64}$)"}, + } ) @_traced("api.mutate_decky") async def api_mutate_decky( diff --git a/decnet/web/router/swarm/api_enroll_host.py b/decnet/web/router/swarm/api_enroll_host.py index b6edb89..351a922 100644 --- a/decnet/web/router/swarm/api_enroll_host.py +++ b/decnet/web/router/swarm/api_enroll_host.py @@ -29,7 +29,11 @@ router = APIRouter() response_model=SwarmEnrolledBundle, status_code=status.HTTP_201_CREATED, tags=["Swarm Hosts"], - responses={409: {"description": "A worker with this name is already enrolled"}}, + responses={ + 400: {"description": "Bad Request (malformed JSON body)"}, + 409: {"description": "A worker with this name is already enrolled"}, + 422: {"description": "Request body validation error"}, + }, ) async def api_enroll_host( req: SwarmEnrollRequest, diff --git a/decnet/web/router/swarm/api_heartbeat.py b/decnet/web/router/swarm/api_heartbeat.py index d1cdac1..52487ca 100644 --- a/decnet/web/router/swarm/api_heartbeat.py +++ b/decnet/web/router/swarm/api_heartbeat.py @@ -101,8 +101,10 @@ async def _verify_peer_matches_host( status_code=204, tags=["Swarm Health"], responses={ + 400: {"description": "Bad Request (malformed JSON body)"}, 403: {"description": "Peer cert missing, or its fingerprint does not match the host's pinned cert"}, 404: {"description": "host_uuid is not enrolled"}, + 422: {"description": "Request body validation error"}, }, ) async def heartbeat( diff --git a/decnet/web/router/swarm/api_teardown_swarm.py b/decnet/web/router/swarm/api_teardown_swarm.py index 0cb68b1..d62f013 100644 --- a/decnet/web/router/swarm/api_teardown_swarm.py +++ b/decnet/web/router/swarm/api_teardown_swarm.py @@ -25,7 +25,11 @@ router = APIRouter() "/teardown", response_model=SwarmDeployResponse, tags=["Swarm Deployments"], - responses={404: {"description": "A targeted host does not exist"}}, + responses={ + 400: {"description": "Bad Request (malformed JSON body)"}, + 404: {"description": "A targeted host does not exist"}, + 422: {"description": "Request body validation error"}, + }, ) async def api_teardown_swarm( req: SwarmTeardownRequest, diff --git a/decnet/web/router/swarm_mgmt/api_decommission_host.py b/decnet/web/router/swarm_mgmt/api_decommission_host.py index 6c86b1f..d473b34 100644 --- a/decnet/web/router/swarm_mgmt/api_decommission_host.py +++ b/decnet/web/router/swarm_mgmt/api_decommission_host.py @@ -24,6 +24,12 @@ router = APIRouter() "/hosts/{uuid}", status_code=status.HTTP_204_NO_CONTENT, tags=["Swarm Management"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, + 404: {"description": "Host not found"}, + 422: {"description": "Path parameter validation error"}, + }, ) async def decommission_host( uuid: str, diff --git a/decnet/web/router/swarm_mgmt/api_enroll_bundle.py b/decnet/web/router/swarm_mgmt/api_enroll_bundle.py index 7232557..799df44 100644 --- a/decnet/web/router/swarm_mgmt/api_enroll_bundle.py +++ b/decnet/web/router/swarm_mgmt/api_enroll_bundle.py @@ -322,6 +322,13 @@ def _render_bootstrap( response_model=EnrollBundleResponse, status_code=status.HTTP_201_CREATED, tags=["Swarm Management"], + responses={ + 400: {"description": "Bad Request (malformed JSON body)"}, + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, + 409: {"description": "A worker with this name is already enrolled"}, + 422: {"description": "Request body validation error"}, + }, ) async def create_enroll_bundle( req: EnrollBundleRequest, diff --git a/decnet/web/router/swarm_mgmt/api_teardown_host.py b/decnet/web/router/swarm_mgmt/api_teardown_host.py index 8cc0732..cae1b73 100644 --- a/decnet/web/router/swarm_mgmt/api_teardown_host.py +++ b/decnet/web/router/swarm_mgmt/api_teardown_host.py @@ -115,6 +115,13 @@ async def _run_teardown( response_model=TeardownHostResponse, status_code=status.HTTP_202_ACCEPTED, tags=["Swarm Management"], + responses={ + 400: {"description": "Bad Request (malformed JSON body)"}, + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, + 404: {"description": "Host not found"}, + 422: {"description": "Request body or path parameter validation error"}, + }, ) async def teardown_host( uuid: str, diff --git a/decnet/web/router/swarm_updates/api_list_host_releases.py b/decnet/web/router/swarm_updates/api_list_host_releases.py index 26d7959..ac493eb 100644 --- a/decnet/web/router/swarm_updates/api_list_host_releases.py +++ b/decnet/web/router/swarm_updates/api_list_host_releases.py @@ -64,6 +64,10 @@ async def _probe_host(host: dict[str, Any]) -> HostReleaseInfo: "/hosts", response_model=HostReleasesResponse, tags=["Swarm Updates"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, + }, ) async def api_list_host_releases( admin: dict = Depends(require_admin), diff --git a/decnet/web/router/swarm_updates/api_push_update.py b/decnet/web/router/swarm_updates/api_push_update.py index c913438..0aea5ee 100644 --- a/decnet/web/router/swarm_updates/api_push_update.py +++ b/decnet/web/router/swarm_updates/api_push_update.py @@ -128,6 +128,13 @@ def _is_expected_connection_drop(exc: BaseException) -> bool: "/push", response_model=PushUpdateResponse, tags=["Swarm Updates"], + responses={ + 400: {"description": "Bad Request (malformed JSON body or conflicting host_uuids/all flags)"}, + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, + 404: {"description": "No matching target hosts or no updater-capable hosts enrolled"}, + 422: {"description": "Request body validation error"}, + }, ) async def api_push_update( req: PushUpdateRequest, diff --git a/decnet/web/router/swarm_updates/api_push_update_self.py b/decnet/web/router/swarm_updates/api_push_update_self.py index 0f289fa..2ffa16f 100644 --- a/decnet/web/router/swarm_updates/api_push_update_self.py +++ b/decnet/web/router/swarm_updates/api_push_update_self.py @@ -68,6 +68,13 @@ async def _push_self_one(host: dict[str, Any], tarball: bytes, sha: str) -> Push "/push-self", response_model=PushUpdateResponse, tags=["Swarm Updates"], + responses={ + 400: {"description": "Bad Request (malformed JSON body or conflicting host_uuids/all flags)"}, + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, + 404: {"description": "No matching target hosts or no updater-capable hosts enrolled"}, + 422: {"description": "Request body validation error"}, + }, ) async def api_push_update_self( req: PushUpdateRequest, diff --git a/decnet/web/router/swarm_updates/api_rollback_host.py b/decnet/web/router/swarm_updates/api_rollback_host.py index 6be74f4..0bfe165 100644 --- a/decnet/web/router/swarm_updates/api_rollback_host.py +++ b/decnet/web/router/swarm_updates/api_rollback_host.py @@ -23,6 +23,13 @@ router = APIRouter() "/rollback", response_model=RollbackResponse, tags=["Swarm Updates"], + responses={ + 400: {"description": "Bad Request (malformed JSON body or host has no updater bundle)"}, + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, + 404: {"description": "Unknown host, or no previous release slot on the worker"}, + 422: {"description": "Request body validation error"}, + }, ) async def api_rollback_host( req: RollbackRequest,