Files
frigate/frigate/api/fastapi_app.py
T
Josh HawkinsandGitHub 7ed7ed56cf
CI / AMD64 Build (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
Miscellaneous fixes (0.18 beta) (#23854)
* fix watchdog process restarts reverting to the boot config

/api/config/set parses a new FrigateConfig and swaps the API and dispatcher onto it, but FrigateApp.config was never rebound, so the watchdog factories rebuilt a crashed process from the config as of startup. Fix is to read through a ConfigHolder that the swap updates.

* fix birdseye camera overrides being clobbered by a global mode change

A global birdseye save published only the global object, leaving the output process to infer which cameras were inheriting by comparing against the previous global mode. That cannot tell an inherited value from an explicit one that happens to match, so it overwrote the override until a restart. Publish the per-camera values the config parse already resolved instead.

* Reject non-finite numbers in GenAI review descriptions

A model returning NaN for confidence or potential_threat_level slipped through the model_construct fallback, which skips validation, and was written into the review segment's JSON data. NaN is not valid JSON, so every subsequent /review request failed with "Out of range float values are not JSON compliant", blanking the review page for any time range containing the poisoned row.

* restore fused DetectionOutput in the OpenVINO SSD model conversion

* fix rgb swap issue for face dataset testing script
2026-07-29 08:39:01 -06:00

191 lines
6.5 KiB
Python

import asyncio
import logging
import re
from fastapi import Depends, FastAPI, Request
from fastapi.responses import JSONResponse
from joserfc.jwk import OctKey
from playhouse.sqliteq import SqliteQueueDatabase
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from starlette_context import middleware, plugins
from starlette_context.plugins import Plugin
from frigate.api import app as main_app
from frigate.api import (
auth,
camera,
chat,
classification,
debug_replay,
event,
export,
media,
motion_search,
notification,
preview,
record,
review,
)
from frigate.api.auth import get_jwt_secret, limiter, require_admin_by_default
from frigate.comms.dispatcher import Dispatcher
from frigate.comms.event_metadata_updater import (
EventMetadataPublisher,
)
from frigate.config import FrigateConfig
from frigate.config.camera.updater import CameraConfigUpdatePublisher
from frigate.config.holder import ConfigHolder
from frigate.config.profile_manager import ProfileManager
from frigate.debug_replay import DebugReplayManager, debug_replay_auto_stop_watchdog
from frigate.embeddings import EmbeddingsContext
from frigate.genai import GenAIClientManager
from frigate.ptz.onvif import OnvifController
from frigate.stats.emitter import StatsEmitter
from frigate.storage import StorageMaintainer
logger = logging.getLogger(__name__)
def check_csrf(request: Request) -> bool:
if request.method in ["GET", "HEAD", "OPTIONS", "TRACE"]:
return True
if "origin" in request.headers and "x-csrf-token" not in request.headers:
return False
return True
# Used to retrieve the remote-user header: https://starlette-context.readthedocs.io/en/latest/plugins.html#easy-mode
class RemoteUserPlugin(Plugin):
key = "Remote-User"
def create_fastapi_app(
frigate_config: FrigateConfig,
database: SqliteQueueDatabase,
embeddings: EmbeddingsContext | None,
detected_frames_processor,
storage_maintainer: StorageMaintainer,
onvif: OnvifController,
stats_emitter: StatsEmitter,
event_metadata_updater: EventMetadataPublisher,
config_publisher: CameraConfigUpdatePublisher,
replay_manager: DebugReplayManager,
dispatcher: Dispatcher | None = None,
profile_manager: ProfileManager | None = None,
enforce_default_admin: bool = True,
config_holder: ConfigHolder | None = None,
):
logger.info("Starting FastAPI app")
app = FastAPI(
debug=False,
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
dependencies=[Depends(require_admin_by_default())]
if enforce_default_admin
else [],
)
# update the request_address with the x-forwarded-for header from nginx
# https://starlette-context.readthedocs.io/en/latest/plugins.html#forwarded-for
app.add_middleware(
middleware.ContextMiddleware,
plugins=(plugins.ForwardedForPlugin(),),
)
# Middleware to connect to DB before and close connection after request
# https://github.com/fastapi/full-stack-fastapi-template/issues/224#issuecomment-737423886
# https://fastapi.tiangolo.com/tutorial/middleware/#before-and-after-the-response
@app.middleware("http")
async def frigate_middleware(request: Request, call_next):
# Before request
if not check_csrf(request):
return JSONResponse(
content={"success": False, "message": "Missing CSRF header"},
status_code=401,
)
if database.is_closed():
database.connect()
response = await call_next(request)
# After request https://stackoverflow.com/a/75487519
if not database.is_closed():
database.close()
return response
@app.on_event("startup")
async def startup():
logger.info("FastAPI started")
asyncio.create_task(
debug_replay_auto_stop_watchdog(
replay_manager, frigate_config, config_publisher
)
)
# Rate limiter (used for login endpoint)
if frigate_config.auth.failed_login_rate_limit is None:
limiter.enabled = False
else:
auth.rateLimiter.set_limit(frigate_config.auth.failed_login_rate_limit)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)
# Routes
# Order of include_router matters: https://fastapi.tiangolo.com/tutorial/path-params/#order-matters
app.include_router(auth.router)
app.include_router(camera.router)
app.include_router(chat.router)
app.include_router(classification.router)
app.include_router(review.router)
app.include_router(main_app.router)
app.include_router(preview.router)
app.include_router(notification.router)
app.include_router(export.router)
app.include_router(event.router)
app.include_router(media.router)
app.include_router(motion_search.router)
app.include_router(record.router)
app.include_router(debug_replay.router)
# App Properties
app.frigate_config = frigate_config
app.genai_manager = GenAIClientManager(frigate_config)
app.embeddings = embeddings
app.detected_frames_processor = detected_frames_processor
app.storage_maintainer = storage_maintainer
app.camera_error_image = None
app.onvif = onvif
app.stats_emitter = stats_emitter
app.event_metadata_updater = event_metadata_updater
app.config_publisher = config_publisher
app.replay_manager = replay_manager
app.dispatcher = dispatcher
app.profile_manager = profile_manager
app.config_holder = config_holder
if frigate_config.auth.enabled:
secret = get_jwt_secret()
key_bytes = None
if isinstance(secret, str):
# If the secret looks like hex (e.g., generated by secrets.token_hex), use raw bytes
if len(secret) % 2 == 0 and re.fullmatch(r"[0-9a-fA-F]+", secret or ""):
try:
key_bytes = bytes.fromhex(secret)
except ValueError:
key_bytes = secret.encode("utf-8")
else:
key_bytes = secret.encode("utf-8")
elif isinstance(secret, (bytes, bytearray)):
key_bytes = bytes(secret)
else:
key_bytes = str(secret).encode("utf-8")
app.jwt_token = OctKey.import_key(key_bytes)
else:
app.jwt_token = None
return app