From 8f5e36a0793e733ddaa14015a4844464251dcc62 Mon Sep 17 00:00:00 2001 From: Ahmed Dlshad Date: Tue, 23 Jun 2026 17:12:32 +0300 Subject: [PATCH] fix(routes): log and cleanly 500 on unreadable HTML page (#4637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(routes): serve 404 instead of 500 when an HTML page file is missing _serve_html_with_nonce opened the HTML file with no error handling, and callers such as /backgrounds and /login pass their paths in with no existence check, so a missing or unreadable file raised an unhandled OSError that surfaced as a 500. Wrap the read and raise HTTPException(404) instead; the normal render path (CSP-nonce substitution) is unchanged. Fixes #4594 Co-Authored-By: Claude Opus 4.8 * fix(routes): distinguish missing page (404) from read failure (500) The previous fix caught a broad OSError and returned 404 for every failure, which masks real server-side problems (permission errors, I/O failures) as "not found" and lets them slip past error alerting. Split FileNotFoundError (genuine 404) from other OSError, which now logs the exception and returns a generic 500 — without leaking the OS error string or file path into the response body. Co-Authored-By: Claude Opus 4.8 * fix(routes): treat unreadable bundled HTML page as logged 500, not 404 Per PR #4637 review: every caller of the page-render helper serves a fixed, server-owned template (index/login/backgrounds), never a client-supplied path. So a missing or unreadable file is a server fault (broken deployment), not a client "not found" — a 404 there mislabels a server error and hides a missing core template from 5xx alerting, contradicting the OSError->500 rationale this PR is built on. Collapse both branches into a single logged, leak-free 500. Move the helper to src.app_helpers.serve_html_with_nonce so the behavior can be unit-tested without importing the whole app (app.py is the slim orchestrator; the test harness stubs src.database, so importing app in tests is not viable). Add tests pinning missing/unreadable -> 500 (not 404) and nonce injection on the happy path. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- app.py | 20 ++++------- src/app_helpers.py | 31 ++++++++++++++++- tests/test_serve_html_with_nonce.py | 52 +++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 tests/test_serve_html_with_nonce.py diff --git a/app.py b/app.py index 57d091efd..07228c482 100644 --- a/app.py +++ b/app.py @@ -44,7 +44,7 @@ from typing import Dict from contextlib import asynccontextmanager from fastapi import FastAPI, Request, HTTPException -from fastapi.responses import JSONResponse, FileResponse, HTMLResponse +from fastapi.responses import JSONResponse, FileResponse from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from starlette.middleware.base import BaseHTTPMiddleware @@ -65,7 +65,7 @@ from core.exceptions import ( import bcrypt as _bcrypt -from src.app_helpers import abs_join +from src.app_helpers import abs_join, serve_html_with_nonce from src.generated_images import GENERATED_IMAGE_HEADERS, resolve_generated_image_path from starlette.responses import RedirectResponse @@ -791,22 +791,14 @@ app.include_router(setup_companion_routes()) # ========= ROUTES (kept in app.py) ========= -def _serve_html_with_nonce(request: Request, file_path: str) -> HTMLResponse: - """Read an HTML file and inject the CSP nonce into inline ', encoding="utf-8") + resp = serve_html_with_nonce(_request_with_nonce("nonce-abc"), str(page)) + assert resp.status_code == 200 + body = resp.body.decode("utf-8") + assert "nonce-abc" in body + assert "{{CSP_NONCE}}" not in body