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

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")

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 "")

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 ──────────────────────────────────

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

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:

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(

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])

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,

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

View File

@@ -50,7 +50,7 @@ def _patch_require_connected(mc=None, *, detail="Radio not connected"):
if mc is None:
return patch(
"app.services.radio_runtime.radio_runtime.require_connected",
side_effect=HTTPException(status_code=503, detail=detail),
side_effect=HTTPException(status_code=423, detail=detail),
)
return patch("app.services.radio_runtime.radio_runtime.require_connected", return_value=mc)
@@ -422,11 +422,11 @@ class TestDebugEndpoint:
class TestRadioDisconnectedHandler:
"""Test that RadioDisconnectedError maps to 503."""
"""Test that RadioDisconnectedError maps to 423."""
@pytest.mark.asyncio
async def test_disconnect_race_returns_503(self, test_db, client):
"""If radio disconnects between require_connected() and lock acquisition, return 503."""
async def test_disconnect_race_returns_423(self, test_db, client):
"""If radio disconnects between require_connected() and lock acquisition, return 423."""
pub_key = "ab" * 32
await _insert_contact(pub_key, "Alice")
@@ -437,7 +437,7 @@ class TestRadioDisconnectedHandler:
"/api/messages/direct", json={"destination": pub_key, "text": "Hi"}
)
assert response.status_code == 503
assert response.status_code == 423
assert "not connected" in response.json()["detail"].lower()
@@ -500,25 +500,25 @@ class TestMessagesEndpoint:
@pytest.mark.asyncio
async def test_send_direct_message_requires_connection(self, test_db, client):
"""Sending message when disconnected returns 503."""
"""Sending message when disconnected returns 423."""
with _patch_require_connected():
response = await client.post(
"/api/messages/direct", json={"destination": "abc123", "text": "Hello"}
)
assert response.status_code == 503
assert response.status_code == 423
assert "not connected" in response.json()["detail"].lower()
@pytest.mark.asyncio
async def test_send_channel_message_requires_connection(self, test_db, client):
"""Sending channel message when disconnected returns 503."""
"""Sending channel message when disconnected returns 423."""
with _patch_require_connected():
response = await client.post(
"/api/messages/channel",
json={"channel_key": "0123456789ABCDEF0123456789ABCDEF", "text": "Hello"},
)
assert response.status_code == 503
assert response.status_code == 423
@pytest.mark.asyncio
async def test_send_direct_message_emits_websocket_message_event(self, test_db, client):
@@ -603,8 +603,8 @@ class TestMessagesEndpoint:
assert "not found" in response.json()["detail"].lower()
@pytest.mark.asyncio
async def test_send_direct_message_duplicate_returns_500(self, test_db):
"""If MessageRepository.create returns None (duplicate), returns 500."""
async def test_send_direct_message_duplicate_returns_422(self, test_db):
"""If MessageRepository.create returns None (duplicate), returns 422."""
from app.models import SendDirectMessageRequest
from app.routers.messages import send_direct_message
@@ -636,12 +636,12 @@ class TestMessagesEndpoint:
SendDirectMessageRequest(destination=pub_key, text="Hello")
)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert "unexpected duplicate" in exc_info.value.detail.lower()
@pytest.mark.asyncio
async def test_send_channel_message_duplicate_returns_500(self, test_db):
"""If MessageRepository.create returns None (duplicate), returns 500."""
async def test_send_channel_message_duplicate_returns_422(self, test_db):
"""If MessageRepository.create returns None (duplicate), returns 422."""
from app.models import SendChannelMessageRequest
from app.routers.messages import send_channel_message
@@ -672,16 +672,16 @@ class TestMessagesEndpoint:
SendChannelMessageRequest(channel_key=chan_key, text="Hello")
)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert "unexpected duplicate" in exc_info.value.detail.lower()
@pytest.mark.asyncio
async def test_resend_channel_message_requires_connection(self, test_db, client):
"""Resend endpoint returns 503 when radio is disconnected."""
"""Resend endpoint returns 423 when radio is disconnected."""
with _patch_require_connected():
response = await client.post("/api/messages/channel/1/resend")
assert response.status_code == 503
assert response.status_code == 423
assert "not connected" in response.json()["detail"].lower()
@pytest.mark.asyncio

View File

@@ -709,7 +709,7 @@ class TestBotMessageRateLimiting:
patch(
"app.routers.messages.send_direct_message",
new_callable=AsyncMock,
side_effect=HTTPException(status_code=500, detail="Send failed"),
side_effect=HTTPException(status_code=422, detail="Send failed"),
),
):
await process_bot_response(

View File

@@ -317,7 +317,7 @@ class TestPathDiscovery:
mock_broadcast.assert_called_once_with("contact", updated.model_dump())
@pytest.mark.asyncio
async def test_returns_504_when_no_response_is_heard(self, test_db, client):
async def test_returns_408_when_no_response_is_heard(self, test_db, client):
await _insert_contact(KEY_A, "Alice", type=1)
mc = MagicMock()
mc.commands = MagicMock()
@@ -332,7 +332,7 @@ class TestPathDiscovery:
mock_rm.radio_operation = _noop_radio_operation(mc)
response = await client.post(f"/api/contacts/{KEY_A}/path-discovery")
assert response.status_code == 504
assert response.status_code == 408
assert "No path discovery response heard" in response.json()["detail"]

View File

@@ -174,8 +174,8 @@ class TestRadioOperationYield:
class TestRequireConnected:
"""Test the require_connected() FastAPI dependency."""
def test_raises_503_when_setup_in_progress(self):
"""HTTPException 503 is raised when radio is connected but setup is still in progress."""
def test_raises_423_when_setup_in_progress(self):
"""HTTPException 423 is raised when radio is connected but setup is still in progress."""
from fastapi import HTTPException
from app.services.radio_runtime import radio_runtime
@@ -188,11 +188,11 @@ class TestRequireConnected:
with pytest.raises(HTTPException) as exc_info:
radio_runtime.require_connected()
assert exc_info.value.status_code == 503
assert exc_info.value.status_code == 423
assert "initializing" in exc_info.value.detail.lower()
def test_raises_503_when_not_connected(self):
"""HTTPException 503 is raised when radio is not connected."""
def test_raises_423_when_not_connected(self):
"""HTTPException 423 is raised when radio is not connected."""
from fastapi import HTTPException
from app.services.radio_runtime import radio_runtime
@@ -205,7 +205,7 @@ class TestRequireConnected:
with pytest.raises(HTTPException) as exc_info:
radio_runtime.require_connected()
assert exc_info.value.status_code == 503
assert exc_info.value.status_code == 423
def test_returns_meshcore_when_connected_and_setup_complete(self):
"""Returns meshcore instance when radio is connected and setup is complete."""

View File

@@ -131,14 +131,14 @@ class TestGetRadioConfig:
assert response.advert_location_source == "current"
@pytest.mark.asyncio
async def test_returns_503_when_self_info_missing(self):
async def test_returns_423_when_self_info_missing(self):
mc = MagicMock()
mc.self_info = None
with patch("app.routers.radio.radio_manager.require_connected", return_value=mc):
with pytest.raises(HTTPException) as exc:
await get_radio_config()
assert exc.value.status_code == 503
assert exc.value.status_code == 423
class TestUpdateRadioConfig:
@@ -278,7 +278,7 @@ class TestUpdateRadioConfig:
with pytest.raises(HTTPException) as exc:
await update_radio_config(RadioConfigUpdate(path_hash_mode=1))
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to set path hash mode" in str(exc.value.detail)
assert radio_manager.path_hash_mode == 0
mc.commands.send_appstart.assert_not_awaited()
@@ -339,7 +339,7 @@ class TestPrivateKeyImport:
with pytest.raises(HTTPException) as exc:
await set_private_key(PrivateKeyUpdate(private_key="aa" * 64))
assert exc.value.status_code == 500
assert exc.value.status_code == 422
class TestDiscoverMesh:
@@ -699,7 +699,7 @@ class TestTracePath:
assert "not a repeater" in exc.value.detail
@pytest.mark.asyncio
async def test_returns_422_when_no_trace_response_is_heard(self):
async def test_returns_408_when_no_trace_response_is_heard(self):
mc = _mock_meshcore_with_info()
repeater = Contact(
public_key="44" * 32,
@@ -741,7 +741,7 @@ class TestTracePath:
)
)
assert exc.value.status_code == 422
assert exc.value.status_code == 408
assert "No trace response heard" in exc.value.detail
@pytest.mark.asyncio
@@ -850,7 +850,7 @@ class TestTracePath:
with pytest.raises(HTTPException) as exc:
await discover_mesh(RadioDiscoveryRequest(target="sensors"))
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert exc.value.detail == "Failed to start mesh discovery"
@pytest.mark.asyncio
@@ -887,7 +887,7 @@ class TestTracePath:
with pytest.raises(HTTPException) as exc:
await set_private_key(PrivateKeyUpdate(private_key="aa" * 64))
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "keystore" in exc.value.detail.lower()
# Called twice: initial attempt + one retry
assert mock_export.await_count == 2
@@ -926,7 +926,7 @@ class TestAdvertise:
with pytest.raises(HTTPException) as exc:
await send_advertisement()
assert exc.value.status_code == 500
assert exc.value.status_code == 422
@pytest.mark.asyncio
async def test_defaults_to_flood_mode(self):
@@ -1059,7 +1059,7 @@ class TestRebootAndReconnect:
assert result["connected"] is True
@pytest.mark.asyncio
async def test_reconnect_raises_503_on_failure(self):
async def test_reconnect_raises_423_on_failure(self):
mock_rm = MagicMock()
mock_rm.is_connected = False
mock_rm.is_reconnecting = False
@@ -1070,7 +1070,7 @@ class TestRebootAndReconnect:
with pytest.raises(HTTPException) as exc:
await reconnect_radio()
assert exc.value.status_code == 503
assert exc.value.status_code == 423
@pytest.mark.asyncio
async def test_disconnect_pauses_connection_attempts_and_broadcasts_health(self):

View File

@@ -57,12 +57,12 @@ def test_require_connected_preserves_http_semantics():
)
with pytest.raises(HTTPException, match="Radio is initializing") as exc:
runtime.require_connected()
assert exc.value.status_code == 503
assert exc.value.status_code == 423
runtime = RadioRuntime(_Manager(meshcore=None, is_connected=False, is_setup_in_progress=False))
with pytest.raises(HTTPException, match="Radio not connected") as exc:
runtime.require_connected()
assert exc.value.status_code == 503
assert exc.value.status_code == 423
def test_require_connected_returns_fresh_meshcore_after_connectivity_check():

View File

@@ -302,7 +302,7 @@ class TestRepeaterCommandRoute:
with pytest.raises(HTTPException) as exc:
await send_repeater_command(KEY_A, CommandRequest(command="ver"))
assert exc.value.status_code == 500
assert exc.value.status_code == 422
mc.start_auto_message_fetching.assert_awaited_once()
@pytest.mark.asyncio
@@ -502,7 +502,7 @@ class TestTraceRoute:
with pytest.raises(HTTPException) as exc:
await request_trace(KEY_A)
assert exc.value.status_code == 500
assert exc.value.status_code == 422
mc.commands.send_trace.assert_awaited_once_with(
path=KEY_A[:8],
tag=1234,
@@ -510,7 +510,7 @@ class TestTraceRoute:
)
@pytest.mark.asyncio
async def test_wait_timeout_returns_422(self, test_db):
async def test_wait_timeout_returns_408(self, test_db):
mc = _mock_mc()
await _insert_contact(KEY_A, name="Client", contact_type=1)
mc.commands.send_trace = AsyncMock(return_value=_radio_result(EventType.OK))
@@ -524,7 +524,7 @@ class TestTraceRoute:
with pytest.raises(HTTPException) as exc:
await request_trace(KEY_A)
assert exc.value.status_code == 422
assert exc.value.status_code == 408
mc.commands.send_trace.assert_awaited_once_with(
path=KEY_A[:8],
tag=1234,
@@ -745,7 +745,7 @@ class TestRepeaterStatus:
assert response.recv_errors == 42
@pytest.mark.asyncio
async def test_504_on_timeout(self, test_db):
async def test_408_on_timeout(self, test_db):
mc = _mock_mc()
await _insert_contact(KEY_A, name="Repeater", contact_type=2)
mc.commands.req_status_sync = AsyncMock(return_value=None)
@@ -756,7 +756,7 @@ class TestRepeaterStatus:
):
with pytest.raises(HTTPException) as exc:
await repeater_status(KEY_A)
assert exc.value.status_code == 504
assert exc.value.status_code == 408
@pytest.mark.asyncio
async def test_400_not_repeater(self, test_db):
@@ -819,7 +819,7 @@ class TestRepeaterLppTelemetry:
assert response.sensors == []
@pytest.mark.asyncio
async def test_504_on_timeout(self, test_db):
async def test_408_on_timeout(self, test_db):
mc = _mock_mc()
await _insert_contact(KEY_A, name="Repeater", contact_type=2)
mc.commands.req_telemetry_sync = AsyncMock(return_value=None)
@@ -830,7 +830,7 @@ class TestRepeaterLppTelemetry:
):
with pytest.raises(HTTPException) as exc:
await repeater_lpp_telemetry(KEY_A)
assert exc.value.status_code == 504
assert exc.value.status_code == 408
@pytest.mark.asyncio
async def test_400_not_repeater(self, test_db):
@@ -1234,7 +1234,7 @@ class TestBatchCliFetch:
with pytest.raises(HTTPException) as exc:
await _batch_cli_fetch(contact, "test_op", [("ver", "firmware_version")])
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to add contact to radio" in exc.value.detail
@pytest.mark.asyncio
@@ -1307,7 +1307,7 @@ class TestRepeaterAddContactError:
with pytest.raises(HTTPException) as exc:
await repeater_status(KEY_A)
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to add contact to radio" in exc.value.detail
@pytest.mark.asyncio
@@ -1325,7 +1325,7 @@ class TestRepeaterAddContactError:
with pytest.raises(HTTPException) as exc:
await repeater_lpp_telemetry(KEY_A)
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to add contact to radio" in exc.value.detail
@pytest.mark.asyncio
@@ -1343,7 +1343,7 @@ class TestRepeaterAddContactError:
with pytest.raises(HTTPException) as exc:
await repeater_neighbors(KEY_A)
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to add contact to radio" in exc.value.detail
@pytest.mark.asyncio
@@ -1361,5 +1361,5 @@ class TestRepeaterAddContactError:
with pytest.raises(HTTPException) as exc:
await repeater_acl(KEY_A)
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to add contact to radio" in exc.value.detail

View File

@@ -646,7 +646,7 @@ class TestOutgoingChannelBroadcast:
request = SendChannelMessageRequest(channel_key=chan_key, text="hello")
await send_channel_message(request)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert "regional override" in exc_info.value.detail.lower()
mc.commands.set_channel.assert_not_awaited()
mc.commands.send_chan_msg.assert_not_awaited()
@@ -790,7 +790,7 @@ class TestOutgoingChannelBroadcast:
SendChannelMessageRequest(channel_key=chan_key, text="this will fail")
)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert radio_manager.get_cached_channel_slot(chan_key) is None
@@ -969,7 +969,7 @@ class TestResendChannelMessage:
assert sent_timestamp == now + 1
@pytest.mark.asyncio
async def test_resend_no_radio_response_returns_504_and_creates_no_new_row(self, test_db):
async def test_resend_no_radio_response_returns_408_and_creates_no_new_row(self, test_db):
"""When resend returns None, report unknown outcome and create no new message row."""
mc = _make_mc(name="MyNode")
chan_key = "c1" * 16
@@ -995,7 +995,7 @@ class TestResendChannelMessage:
):
await resend_channel_message(msg_id, new_timestamp=True)
assert exc_info.value.status_code == 504
assert exc_info.value.status_code == 408
assert exc_info.value.detail == NO_RADIO_RESPONSE_AFTER_SEND_DETAIL
messages = await MessageRepository.get_all(
@@ -1317,7 +1317,7 @@ class TestPathHashModeOverride:
SendChannelMessageRequest(channel_key=chan_key, text="hello")
)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert "path hash mode" in exc_info.value.detail.lower()
mc.commands.send_chan_msg.assert_not_awaited()
@@ -1567,7 +1567,7 @@ class TestRadioExceptionMidSend:
assert len(messages) == 0
@pytest.mark.asyncio
async def test_dm_send_no_radio_response_returns_504_without_storing_message(self, test_db):
async def test_dm_send_no_radio_response_returns_408_without_storing_message(self, test_db):
"""When mc.commands.send_msg() returns None, report unknown outcome and store nothing."""
mc = _make_mc()
pub_key = "ac" * 32
@@ -1584,7 +1584,7 @@ class TestRadioExceptionMidSend:
SendDirectMessageRequest(destination=pub_key, text="Did this send?")
)
assert exc_info.value.status_code == 504
assert exc_info.value.status_code == 408
assert exc_info.value.detail == NO_RADIO_RESPONSE_AFTER_SEND_DETAIL
messages = await MessageRepository.get_all(
@@ -1593,7 +1593,7 @@ class TestRadioExceptionMidSend:
assert len(messages) == 0
@pytest.mark.asyncio
async def test_channel_send_no_radio_response_returns_504_without_storing_message(
async def test_channel_send_no_radio_response_returns_408_without_storing_message(
self, test_db
):
"""When mc.commands.send_chan_msg() returns None, report unknown outcome and store nothing."""
@@ -1612,7 +1612,7 @@ class TestRadioExceptionMidSend:
SendChannelMessageRequest(channel_key=chan_key, text="Did this send?")
)
assert exc_info.value.status_code == 504
assert exc_info.value.status_code == 408
assert exc_info.value.detail == NO_RADIO_RESPONSE_AFTER_SEND_DETAIL
messages = await MessageRepository.get_all(
@@ -1733,7 +1733,7 @@ class TestRadioExceptionMidSend:
SendChannelMessageRequest(channel_key=chan_key_b, text="Never sent")
)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert radio_manager.get_cached_channel_slot(chan_key_a) is None
assert radio_manager.get_cached_channel_slot(chan_key_b) is None
mc.commands.send_chan_msg.assert_not_called()