Return a specific 404 when starting a debug replay with no recordings in range (#24024)

This commit is contained in:
Josh Hawkins
2026-08-18 10:08:52 -05:00
committed by GitHub
parent 8384a8c5b3
commit 036bae4ea9
7 changed files with 54 additions and 5 deletions
+3 -1
View File
@@ -7109,7 +7109,9 @@ paths:
schema: schema:
$ref: '#/components/schemas/DebugReplayStartResponse' $ref: '#/components/schemas/DebugReplayStartResponse'
'400': '400':
description: Invalid camera, time range, or no recordings description: Invalid camera or time range
'404':
description: No recordings in the requested time range
'409': '409':
description: A replay session is already active description: A replay session is already active
'422': '422':
+11 -1
View File
@@ -13,6 +13,7 @@ from frigate.api.auth import require_role
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.jobs.debug_replay import ( from frigate.jobs.debug_replay import (
ExportDebugReplaySource, ExportDebugReplaySource,
NoRecordingsError,
RecordingDebugReplaySource, RecordingDebugReplaySource,
start_debug_replay_job, start_debug_replay_job,
) )
@@ -74,7 +75,8 @@ class DebugReplayStopResponse(BaseModel):
response_model=DebugReplayStartResponse, response_model=DebugReplayStartResponse,
status_code=202, status_code=202,
responses={ responses={
400: {"description": "Invalid camera, time range, or no recordings"}, 400: {"description": "Invalid camera or time range"},
404: {"description": "No recordings in the requested time range"},
409: {"description": "A replay session is already active"}, 409: {"description": "A replay session is already active"},
}, },
dependencies=[Depends(require_role(["admin"]))], dependencies=[Depends(require_role(["admin"]))],
@@ -113,6 +115,14 @@ async def start_debug_replay(request: Request, body: DebugReplayStartBody):
}, },
status_code=409, status_code=409,
) )
except NoRecordingsError:
return JSONResponse(
content={
"success": False,
"message": "No recordings found in the selected time range",
},
status_code=404,
)
except ValueError: except ValueError:
logger.exception("Rejected debug replay start request") logger.exception("Rejected debug replay start request")
return JSONResponse( return JSONResponse(
+5 -1
View File
@@ -115,6 +115,10 @@ def query_recordings(source_camera: str, start_ts: float, end_ts: float) -> Mode
return cast(ModelSelect, query) return cast(ModelSelect, query)
class NoRecordingsError(ValueError):
"""Raised when no recordings exist in the requested time range."""
class DebugReplaySource(ABC): class DebugReplaySource(ABC):
"""Abstract source for a debug replay session. """Abstract source for a debug replay session.
@@ -187,7 +191,7 @@ class RecordingDebugReplaySource(DebugReplaySource):
raise ValueError("End time must be after start time") raise ValueError("End time must be after start time")
if not query_recordings(self._camera, self._start_ts, self._end_ts).count(): if not query_recordings(self._camera, self._start_ts, self._end_ts).count():
raise ValueError( raise NoRecordingsError(
f"No recordings found for camera '{self._camera}' in the specified time range" f"No recordings found for camera '{self._camera}' in the specified time range"
) )
@@ -2,6 +2,7 @@
from unittest.mock import patch from unittest.mock import patch
from frigate.jobs.debug_replay import NoRecordingsError
from frigate.models import Event, Recordings, ReviewSegment from frigate.models import Event, Recordings, ReviewSegment
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
@@ -66,6 +67,32 @@ class TestDebugReplayAPI(BaseTestHttp):
# (CodeQL: information exposure through an exception). # (CodeQL: information exposure through an exception).
self.assertEqual(body["message"], "Invalid debug replay parameters") self.assertEqual(body["message"], "Invalid debug replay parameters")
def test_start_returns_404_when_no_recordings(self):
with patch(
"frigate.api.debug_replay.start_debug_replay_job",
side_effect=NoRecordingsError(
"No recordings found for camera 'front' in the specified time range"
),
):
with AuthTestClient(self.app) as client:
resp = client.post(
"/debug_replay/start",
json={
"camera": "front",
"start_time": 100,
"end_time": 200,
},
)
self.assertEqual(resp.status_code, 404)
body = resp.json()
self.assertFalse(body["success"])
# Message is hard-coded so we don't echo exception text back to clients
# (CodeQL: information exposure through an exception).
self.assertEqual(
body["message"], "No recordings found in the selected time range"
)
def test_start_returns_409_when_session_already_active(self): def test_start_returns_409_when_session_already_active(self):
with patch( with patch(
"frigate.api.debug_replay.start_debug_replay_job", "frigate.api.debug_replay.start_debug_replay_job",
+2 -1
View File
@@ -9,6 +9,7 @@ from unittest.mock import MagicMock, patch
from frigate.debug_replay import DebugReplayManager from frigate.debug_replay import DebugReplayManager
from frigate.jobs.debug_replay import ( from frigate.jobs.debug_replay import (
DebugReplayJob, DebugReplayJob,
NoRecordingsError,
RecordingDebugReplaySource, RecordingDebugReplaySource,
cancel_debug_replay_job, cancel_debug_replay_job,
get_active_runner, get_active_runner,
@@ -129,7 +130,7 @@ class TestStartDebugReplayJob(unittest.TestCase):
empty_qs = MagicMock() empty_qs = MagicMock()
empty_qs.count.return_value = 0 empty_qs.count.return_value = 0
with patch("frigate.jobs.debug_replay.query_recordings", return_value=empty_qs): with patch("frigate.jobs.debug_replay.query_recordings", return_value=empty_qs):
with self.assertRaises(ValueError): with self.assertRaises(NoRecordingsError):
start_debug_replay_job( start_debug_replay_job(
source=RecordingDebugReplaySource( source=RecordingDebugReplaySource(
source_camera="front", source_camera="front",
+1
View File
@@ -21,6 +21,7 @@
"toast": { "toast": {
"error": "Failed to start debug replay: {{error}}", "error": "Failed to start debug replay: {{error}}",
"alreadyActive": "A replay session is already active", "alreadyActive": "A replay session is already active",
"noRecordings": "No recordings found in the selected time range",
"stopError": "Failed to stop debug replay: {{error}}", "stopError": "Failed to stop debug replay: {{error}}",
"goToReplay": "Go to Replay" "goToReplay": "Go to Replay"
} }
@@ -217,7 +217,11 @@ export default function DebugReplayDialog({
error.response?.data?.detail || error.response?.data?.detail ||
"Unknown error"; "Unknown error";
if (error.response?.status === 409) { if (error.response?.status === 404) {
toast.error(t("dialog.toast.noRecordings"), {
position: "top-center",
});
} else if (error.response?.status === 409) {
toast.error(t("dialog.toast.alreadyActive"), { toast.error(t("dialog.toast.alreadyActive"), {
position: "top-center", position: "top-center",
closeButton: true, closeButton: true,