fix frozen time bounds on the recordings API (#24121)

`after` and `before` defaulted to `datetime.now()` in the function signature, so they were evaluated once at import. Requests that omitted them got a window ending at process start. They now resolve in the handler.
This commit is contained in:
Josh Hawkins
2026-09-12 07:30:04 -06:00
committed by Nicolas Mowen
parent e2da7aae99
commit 9416e74aeb
4 changed files with 38 additions and 10 deletions
+6 -2
View File
@@ -7197,13 +7197,17 @@ paths:
in: query
required: false
schema:
type: number
anyOf:
- type: number
- type: 'null'
title: After
- name: before
in: query
required: false
schema:
type: number
anyOf:
- type: number
- type: 'null'
title: Before
responses:
'200':
+5 -2
View File
@@ -358,10 +358,13 @@ async def recordings_coverage(
@router.get("/{camera_name}/recordings", dependencies=[Depends(require_camera_access)])
async def recordings(
camera_name: str,
after: float = (datetime.now() - timedelta(hours=1)).timestamp(),
before: float = datetime.now().timestamp(),
after: float | None = None,
before: float | None = None,
):
"""Return specific camera recordings between the given 'after'/'end' times. If not provided the last hour will be used"""
now = datetime.now()
after = after if after is not None else (now - timedelta(hours=1)).timestamp()
before = before if before is not None else now.timestamp()
recordings = (
Recordings.select(
Recordings.id,
+23 -1
View File
@@ -1,7 +1,8 @@
"""Unit tests for recordings/media API endpoints."""
from dataclasses import dataclass
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
from unittest.mock import patch
import pytz
from fastapi import Request
@@ -1838,6 +1839,27 @@ class TestHttpMedia(BaseTestHttp):
recording["id"] for recording in response.json()
] == expected_ids
def test_recordings_default_range_follows_the_request_clock(self):
"""Omitted bounds resolve at request time, not at module import time."""
uptime = timedelta(hours=2)
recorded_at = (datetime.now() + uptime).timestamp()
class UptimeDatetime(datetime):
"""Stands in for a process that booted `uptime` ago."""
@classmethod
def now(cls, tz=None):
return datetime.now(tz) + uptime
with AuthTestClient(self.app) as client:
self._insert_recording("after_boot", recorded_at - 20, recorded_at - 10)
with patch("frigate.api.record.datetime", UptimeDatetime):
response = client.get("/front_door/recordings")
assert response.status_code == 200
assert [recording["id"] for recording in response.json()] == ["after_boot"]
def test_vod_handles_all_range_relations(self):
"""VOD clips every interval relation with positive playback duration."""
with AuthTestClient(self.app) as client:
+4 -5
View File
@@ -410,11 +410,10 @@ def enrich(spec: dict, access_map: dict) -> tuple[dict, list, list]:
# Numeric defaults at or above this magnitude are treated as live Unix
# timestamps baked into the schema at import time (e.g. the /{camera_name}
# /recordings after/before params default to datetime.now()). They make the
# export non-deterministic and document a meaningless frozen epoch, so they are
# stripped. The proper fix is to default those route params to None and resolve
# "now" inside the handler.
# timestamps baked into the schema at import time. They make the export
# non-deterministic and document a meaningless frozen epoch, so they are
# stripped. No route defaults this way today, and route params that need "now"
# resolve it inside the handler. Kept as a guard against the pattern returning.
VOLATILE_DEFAULT_THRESHOLD = 1_000_000_000