mirror of
https://github.com/alexhopeoconnor/frigate.git
synced 2026-10-04 03:18:11 +10:00
Return a specific 404 when starting a debug replay with no recordings in range (#24024)
This commit is contained in:
Vendored
+3
-1
@@ -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':
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user