From cd96cb3e60b804422a8c51ad0fb8906c68cd1981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 25 Oct 2026 13:00:57 +0100 Subject: [PATCH] fix(proxy): keep API paths ending in a file extension behind the proxy The proxy matcher left out every path ending in an image or text extension so static files skip it. The exclusion was not anchored, so an API path whose last segment the caller chooses (a custom tag name ending in .png) skipped the proxy too, and with it the demo-mode write refusal, the worker-only refusal and the security headers. Authentication was unaffected. The extension exclusion now applies outside /api/ only. --- src/__tests__/proxy-body-limit.test.ts | 9 +++++++++ src/proxy.ts | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/__tests__/proxy-body-limit.test.ts b/src/__tests__/proxy-body-limit.test.ts index 07bb01a09..f9fab8f7a 100644 --- a/src/__tests__/proxy-body-limit.test.ts +++ b/src/__tests__/proxy-body-limit.test.ts @@ -102,9 +102,18 @@ describe("proxy matcher", () => { "/api/import/csv/preview", // The composition PUT beside the label upload stays behind the proxy. "/api/medications/abc/ingredients", + // An API path ending in a static-file extension is still an API call: + // the last segment is the caller's choice. + "/api/mood/tags/custom/x.png", + "/api/documents/inbound/abc.txt", + "/api/measurements/x.svg", ]) { expect(re.test(path), path).toBe(true); } + // Static files outside the API still skip it. + for (const path of ["/icons/icon-192.png", "/og.jpg", "/robots.txt"]) { + expect(re.test(path), path).toBe(false); + } }); }); diff --git a/src/proxy.ts b/src/proxy.ts index 132e9c31a..d5227364c 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -857,14 +857,14 @@ export function proxy(request: NextRequest) { export const config = { matcher: [ - // Apply to all routes except static files, Next.js internals, SW, and - // manifest — and except the large-upload API routes listed in + // Apply to all routes except static files outside `/api/`, Next.js + // internals, SW, and manifest — and except the large-upload API routes in // `src/lib/http/proxy-bypass-routes.ts`. Next reads a request body into // memory for the proxy before the route runs, so a route whose body may // exceed `proxyClientMaxBodySize` (1 MB, next.config.ts) is left out of // the matcher and `apiHandler` performs the proxy's duties for it. The // alternation must stay equal to that list; `proxy-body-limit.test.ts` // compiles this exactly as Next does and checks. - "/((?!_next/static|_next/image|favicon.ico|sw\\.js|manifest\\.json|robots\\.txt|sitemap\\.xml|api/(?:import(?:/csv|/labs-csv|/medications-csv|/apple-health-export|/health-connect-export)?|admin/import-apple-health-export|admin/backups/upload|documents/inbound|medications/intake/(?:dose-history-import|bulk)|labs/ocr/extract|medications/[^/]+/ingredients/extract|user/avatar|measurements/batch|workouts/batch|insights/ecg|mood-entries/bulk|cycle/day-logs/bulk)/?$|.*\\.(?:svg|png|jpg|jpeg|gif|webp|txt|xml)$).*)", + "/((?!_next/static|_next/image|favicon.ico|sw\\.js|manifest\\.json|robots\\.txt|sitemap\\.xml|api/(?:import(?:/csv|/labs-csv|/medications-csv|/apple-health-export|/health-connect-export)?|admin/import-apple-health-export|admin/backups/upload|documents/inbound|medications/intake/(?:dose-history-import|bulk)|labs/ocr/extract|medications/[^/]+/ingredients/extract|user/avatar|measurements/batch|workouts/batch|insights/ecg|mood-entries/bulk|cycle/day-logs/bulk)/?$|(?!api/).*\\.(?:svg|png|jpg|jpeg|gif|webp|txt|xml)$).*)", ], };