Do full rewrite of 5xx => 4xx

This commit is contained in:
Jack Kingsman
2026-04-30 18:47:35 -07:00
parent f710a1f2d9
commit dbf14259dc
17 changed files with 114 additions and 114 deletions
+2 -2
View File
@@ -176,8 +176,8 @@ app.add_middleware(
@app.exception_handler(RadioDisconnectedError)
async def radio_disconnected_handler(request: Request, exc: RadioDisconnectedError):
"""Return 503 when a radio disconnect race occurs during an operation."""
return JSONResponse(status_code=503, content={"detail": "Radio not connected"})
"""Return 423 when a radio disconnect race occurs during an operation."""
return JSONResponse(status_code=423, content={"detail": "Radio not connected"})
@app.middleware("http")
+6 -6
View File
@@ -66,11 +66,11 @@ async def _resolve_contact_or_404(
async def _ensure_on_radio(mc, contact: Contact) -> None:
"""Add a contact to the radio for routing, raising 500 on failure."""
"""Add a contact to the radio for routing, raising 422 on failure."""
add_result = await mc.commands.add_contact(contact.to_radio_dict())
if add_result is not None and add_result.type == EventType.ERROR:
raise HTTPException(
status_code=500, detail=f"Failed to add contact to radio: {add_result.payload}"
status_code=422, detail=f"Failed to add contact to radio: {add_result.payload}"
)
@@ -452,7 +452,7 @@ async def request_trace(public_key: str) -> TraceResponse:
)
if result.type == EventType.ERROR:
raise HTTPException(status_code=500, detail=f"Failed to send trace: {result.payload}")
raise HTTPException(status_code=422, detail=f"Failed to send trace: {result.payload}")
# Wait for the matching TRACE_DATA event
event = await mc.wait_for_event(
@@ -462,7 +462,7 @@ async def request_trace(public_key: str) -> TraceResponse:
)
if event is None:
raise HTTPException(status_code=422, detail="No trace response heard")
raise HTTPException(status_code=408, detail="No trace response heard")
trace = event.payload
path = trace.get("path", [])
@@ -506,7 +506,7 @@ async def request_path_discovery(public_key: str) -> PathDiscoveryResponse:
result = await mc.commands.send_path_discovery(contact.public_key)
if result.type == EventType.ERROR:
raise HTTPException(
status_code=500,
status_code=422,
detail=f"Failed to send path discovery: {result.payload}",
)
@@ -518,7 +518,7 @@ async def request_path_discovery(public_key: str) -> PathDiscoveryResponse:
await response_task
if event is None:
raise HTTPException(status_code=504, detail="No path discovery response heard")
raise HTTPException(status_code=408, detail="No path discovery response heard")
payload = event.payload
forward_path = str(payload.get("out_path") or "")
+5 -5
View File
@@ -48,7 +48,7 @@ async def vapid_public_key() -> VapidPublicKeyResponse:
"""Return the VAPID public key for browser PushManager.subscribe()."""
key = get_vapid_public_key()
if not key:
raise HTTPException(status_code=503, detail="VAPID keys not initialized")
raise HTTPException(status_code=423, detail="VAPID keys not initialized")
return VapidPublicKeyResponse(public_key=key)
@@ -103,7 +103,7 @@ async def test_push(subscription_id: str) -> dict:
vapid_key = get_vapid_private_key()
if not vapid_key:
raise HTTPException(status_code=503, detail="VAPID keys not initialized")
raise HTTPException(status_code=423, detail="VAPID keys not initialized")
payload = json.dumps(
{
@@ -127,7 +127,7 @@ async def test_push(subscription_id: str) -> dict:
)
return {"status": "sent"}
except TimeoutError:
raise HTTPException(status_code=504, detail="Push delivery timed out") from None
raise HTTPException(status_code=408, detail="Push delivery timed out") from None
except WebPushException as e:
status_code = getattr(getattr(e, "response", None), "status_code", 0)
if status_code in (403, 404, 410):
@@ -143,10 +143,10 @@ async def test_push(subscription_id: str) -> dict:
"Re-enable push from a conversation header.",
) from None
logger.warning("Test push failed: %s", e)
raise HTTPException(status_code=502, detail=f"Push delivery failed: {e}") from None
raise HTTPException(status_code=422, detail=f"Push delivery failed: {e}") from None
except Exception as e:
logger.warning("Test push failed: %s", e)
raise HTTPException(status_code=502, detail=f"Push delivery failed: {e}") from None
raise HTTPException(status_code=422, detail=f"Push delivery failed: {e}") from None
# ── Global push conversation management ──────────────────────────────────
+15 -15
View File
@@ -338,7 +338,7 @@ async def get_radio_config() -> RadioConfigResponse:
info = mc.self_info
if not info:
raise HTTPException(status_code=503, detail="Radio info not available")
raise HTTPException(status_code=423, detail="Radio info not available")
adv_loc_policy = info.get("adv_loc_policy", 1)
advert_location_source: AdvertLocationSource = "off" if adv_loc_policy == 0 else "current"
@@ -380,7 +380,7 @@ async def update_radio_config(update: RadioConfigUpdate) -> RadioConfigResponse:
except PathHashModeUnsupportedError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except RadioCommandRejectedError as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
raise HTTPException(status_code=422, detail=str(exc)) from exc
return await get_radio_config()
@@ -430,7 +430,7 @@ async def set_private_key(update: PrivateKeyUpdate) -> dict:
export_and_store_private_key_fn=export_and_store_private_key,
)
except (RadioCommandRejectedError, KeystoreRefreshError) as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
raise HTTPException(status_code=422, detail=str(exc)) from exc
return {"status": "ok"}
@@ -454,7 +454,7 @@ async def send_advertisement(request: RadioAdvertiseRequest | None = None) -> di
success = await do_send_advertisement(mc, force=True, mode=mode)
if not success:
raise HTTPException(status_code=500, detail=f"Failed to send {mode} advertisement")
raise HTTPException(status_code=422, detail=f"Failed to send {mode} advertisement")
return {"status": "ok"}
@@ -486,7 +486,7 @@ async def discover_mesh(request: RadioDiscoveryRequest) -> RadioDiscoveryRespons
tag=tag,
)
if send_result is None or send_result.type == EventType.ERROR:
raise HTTPException(status_code=500, detail="Failed to start mesh discovery")
raise HTTPException(status_code=422, detail="Failed to start mesh discovery")
deadline = _monotonic() + DISCOVERY_WINDOW_SECONDS
results_by_key: dict[str, RadioDiscoveryResult] = {}
@@ -538,7 +538,7 @@ async def trace_path(request: RadioTraceRequest) -> RadioTraceResponse:
async with radio_manager.radio_operation("radio_trace", pause_polling=True) as mc:
local_public_key = str((mc.self_info or {}).get("public_key") or "").lower()
if len(local_public_key) != 64:
raise HTTPException(status_code=503, detail="Local radio public key is unavailable")
raise HTTPException(status_code=423, detail="Local radio public key is unavailable")
local_name = (mc.self_info or {}).get("name")
response_task = asyncio.create_task(
@@ -555,13 +555,13 @@ async def trace_path(request: RadioTraceRequest) -> RadioTraceResponse:
flags=trace_flags,
)
if send_result is None or send_result.type == EventType.ERROR:
raise HTTPException(status_code=500, detail="Failed to send trace")
raise HTTPException(status_code=422, detail="Failed to send trace")
timeout_seconds = _trace_timeout_seconds(send_result)
try:
event = await asyncio.wait_for(response_task, timeout=timeout_seconds)
except TimeoutError as exc:
raise HTTPException(status_code=422, detail="No trace response heard") from exc
raise HTTPException(status_code=408, detail="No trace response heard") from exc
finally:
if not response_task.done():
response_task.cancel()
@@ -569,12 +569,12 @@ async def trace_path(request: RadioTraceRequest) -> RadioTraceResponse:
await response_task
if event is None:
raise HTTPException(status_code=422, detail="No trace response heard")
raise HTTPException(status_code=408, detail="No trace response heard")
payload = event.payload if isinstance(event.payload, dict) else {}
path_len = payload.get("path_len")
if not isinstance(path_len, int):
raise HTTPException(status_code=500, detail="Trace response was malformed")
raise HTTPException(status_code=422, detail="Trace response was malformed")
raw_path = payload.get("path")
path_nodes = raw_path if isinstance(raw_path, list) else []
@@ -588,7 +588,7 @@ async def trace_path(request: RadioTraceRequest) -> RadioTraceResponse:
hashed_nodes = path_nodes[:-1] if final_local_node is not None else path_nodes
if len(hashed_nodes) < len(trace_nodes):
raise HTTPException(status_code=500, detail="Trace response was incomplete")
raise HTTPException(status_code=422, detail="Trace response was incomplete")
nodes: list[RadioTraceNode] = []
for index, trace_node in enumerate(trace_nodes):
@@ -641,13 +641,13 @@ async def _attempt_reconnect() -> dict:
except Exception as e:
logger.exception("Post-connect setup failed after reconnect")
raise HTTPException(
status_code=503,
status_code=423,
detail=f"Radio connected but setup failed: {e}",
) from e
if not success:
raise HTTPException(
status_code=503, detail="Failed to reconnect. Check radio connection and power."
status_code=423, detail="Failed to reconnect. Check radio connection and power."
)
return {"status": "ok", "message": "Reconnected successfully", "connected": True}
@@ -702,14 +702,14 @@ async def reconnect_radio() -> dict:
logger.info("Radio connected but setup incomplete, retrying setup")
try:
if not await _prepare_connected(broadcast_on_success=True):
raise HTTPException(status_code=503, detail="Radio connection is paused")
raise HTTPException(status_code=423, detail="Radio connection is paused")
return {"status": "ok", "message": "Setup completed", "connected": True}
except HTTPException:
raise
except Exception as e:
logger.exception("Post-connect setup failed")
raise HTTPException(
status_code=503,
status_code=423,
detail=f"Radio connected but setup failed: {e}",
) from e
+2 -2
View File
@@ -113,7 +113,7 @@ async def repeater_status(public_key: str) -> RepeaterStatusResponse:
logger.debug("LPP sensor fetch failed for %s (non-fatal): %s", public_key[:12], e)
if status is None:
raise HTTPException(status_code=504, detail="No status response from repeater")
raise HTTPException(status_code=408, detail="No status response from repeater")
response = RepeaterStatusResponse(
battery_volts=status.get("bat", 0) / 1000.0,
@@ -222,7 +222,7 @@ async def repeater_lpp_telemetry(public_key: str) -> RepeaterLppTelemetryRespons
)
if telemetry is None:
raise HTTPException(status_code=504, detail="No telemetry response from repeater")
raise HTTPException(status_code=408, detail="No telemetry response from repeater")
sensors: list[LppSensor] = []
for entry in telemetry:
+2 -2
View File
@@ -58,7 +58,7 @@ async def room_status(public_key: str) -> RepeaterStatusResponse:
status = await mc.commands.req_status_sync(contact.public_key, timeout=10, min_timeout=5)
if status is None:
raise HTTPException(status_code=504, detail="No status response from room server")
raise HTTPException(status_code=408, detail="No status response from room server")
return RepeaterStatusResponse(
battery_volts=status.get("bat", 0) / 1000.0,
@@ -98,7 +98,7 @@ async def room_lpp_telemetry(public_key: str) -> RepeaterLppTelemetryResponse:
)
if telemetry is None:
raise HTTPException(status_code=504, detail="No telemetry response from room server")
raise HTTPException(status_code=408, detail="No telemetry response from room server")
sensors = [
LppSensor(
+1 -1
View File
@@ -291,7 +291,7 @@ async def send_contact_cli_command(
if send_result.type == EventType.ERROR:
raise HTTPException(
status_code=500, detail=f"Failed to send command: {send_result.payload}"
status_code=422, detail=f"Failed to send command: {send_result.payload}"
)
response_event = await fetch_contact_cli_response(mc, contact.public_key[:12])
+16 -16
View File
@@ -159,7 +159,7 @@ async def send_channel_message_with_effective_scope(
override_result.payload,
)
raise HTTPException(
status_code=500,
status_code=422,
detail=(
f"Failed to apply regional override {override_scope!r} before {action_label}: "
f"{override_result.payload}"
@@ -189,7 +189,7 @@ async def send_channel_message_with_effective_scope(
phm_result.payload,
)
raise HTTPException(
status_code=500,
status_code=422,
detail=(
f"Failed to apply path hash mode override before {action_label}: "
f"{phm_result.payload}"
@@ -233,7 +233,7 @@ async def send_channel_message_with_effective_scope(
set_result.payload,
)
raise HTTPException(
status_code=500,
status_code=422,
detail=f"Failed to configure channel on radio before {action_label}",
)
radio_manager.note_channel_slot_loaded(channel_key, channel_slot)
@@ -256,7 +256,7 @@ async def send_channel_message_with_effective_scope(
action_label,
channel.name,
)
raise HTTPException(status_code=504, detail=NO_RADIO_RESPONSE_AFTER_SEND_DETAIL)
raise HTTPException(status_code=408, detail=NO_RADIO_RESPONSE_AFTER_SEND_DETAIL)
if send_result.type == EventType.ERROR:
logger.error(
"Radio returned error during %s for channel %s: %s",
@@ -598,10 +598,10 @@ async def send_direct_message_to_contact(
"No response from radio after direct send to %s; send outcome is unknown",
contact.public_key[:12],
)
raise HTTPException(status_code=504, detail=NO_RADIO_RESPONSE_AFTER_SEND_DETAIL)
raise HTTPException(status_code=408, detail=NO_RADIO_RESPONSE_AFTER_SEND_DETAIL)
if result.type == EventType.ERROR:
raise HTTPException(status_code=500, detail=f"Failed to send message: {result.payload}")
raise HTTPException(status_code=422, detail=f"Failed to send message: {result.payload}")
message = await create_outgoing_direct_message(
conversation_key=contact.public_key.lower(),
@@ -613,7 +613,7 @@ async def send_direct_message_to_contact(
)
if message is None:
raise HTTPException(
status_code=500,
status_code=422,
detail="Failed to store outgoing message - unexpected duplicate",
)
finally:
@@ -626,7 +626,7 @@ async def send_direct_message_to_contact(
)
if sent_at is None or sender_timestamp is None or message is None or result is None:
raise HTTPException(status_code=500, detail="Failed to store outgoing message")
raise HTTPException(status_code=422, detail="Failed to store outgoing message")
await contact_repository.update_last_contacted(contact.public_key.lower(), sent_at)
@@ -791,7 +791,7 @@ async def send_channel_message_to_channel(
)
if outgoing_message is None:
raise HTTPException(
status_code=500,
status_code=422,
detail="Failed to store outgoing message - unexpected duplicate",
)
@@ -813,11 +813,11 @@ async def send_channel_message_to_channel(
"No response from radio after channel send to %s; send outcome is unknown",
channel.name,
)
raise HTTPException(status_code=504, detail=NO_RADIO_RESPONSE_AFTER_SEND_DETAIL)
raise HTTPException(status_code=408, detail=NO_RADIO_RESPONSE_AFTER_SEND_DETAIL)
if result.type == EventType.ERROR:
raise HTTPException(
status_code=500, detail=f"Failed to send message: {result.payload}"
status_code=422, detail=f"Failed to send message: {result.payload}"
)
except Exception:
if outgoing_message is not None:
@@ -834,7 +834,7 @@ async def send_channel_message_to_channel(
)
if sent_at is None or sender_timestamp is None or outgoing_message is None:
raise HTTPException(status_code=500, detail="Failed to store outgoing message")
raise HTTPException(status_code=422, detail="Failed to store outgoing message")
outgoing_message = await build_stored_outgoing_channel_message(
message_id=outgoing_message.id,
@@ -928,7 +928,7 @@ async def resend_channel_message_record(
)
if new_message is None:
raise HTTPException(
status_code=500,
status_code=422,
detail="Failed to store resent message - unexpected duplicate",
)
@@ -949,10 +949,10 @@ async def resend_channel_message_record(
"No response from radio after channel resend to %s; send outcome is unknown",
channel.name,
)
raise HTTPException(status_code=504, detail=NO_RADIO_RESPONSE_AFTER_SEND_DETAIL)
raise HTTPException(status_code=408, detail=NO_RADIO_RESPONSE_AFTER_SEND_DETAIL)
if result.type == EventType.ERROR:
raise HTTPException(
status_code=500,
status_code=422,
detail=f"Failed to resend message: {result.payload}",
)
except Exception:
@@ -971,7 +971,7 @@ async def resend_channel_message_record(
if new_timestamp:
if sent_at is None or new_message is None:
raise HTTPException(status_code=500, detail="Failed to assign resend timestamp")
raise HTTPException(status_code=422, detail="Failed to assign resend timestamp")
new_message = await build_stored_outgoing_channel_message(
message_id=new_message.id,
+3 -3
View File
@@ -52,12 +52,12 @@ class RadioRuntime:
def require_connected(self):
"""Return MeshCore when available, mirroring existing HTTP semantics."""
if self.is_setup_in_progress:
raise HTTPException(status_code=503, detail="Radio is initializing")
raise HTTPException(status_code=423, detail="Radio is initializing")
if not self.is_connected:
raise HTTPException(status_code=503, detail="Radio not connected")
raise HTTPException(status_code=423, detail="Radio not connected")
mc = self.meshcore
if mc is None:
raise HTTPException(status_code=503, detail="Radio not connected")
raise HTTPException(status_code=423, detail="Radio not connected")
return mc
@asynccontextmanager