Fixing Nginx 404 on SvelteKit dynamic routes

by Michał Węglarz

I've just dedicated a few long hours of my life to finding out why the routing in my SvelteKit application, running in a Docker container on a VPS, behind the Nginx reverse proxy, doesn't work properly when deployed in production, while locally I couldn't find any issue.

The problem

The client-side navigation, e.g. when clicking in-app links, worked perfectly fine.

/home -> /products/[id]/details

But when I tried to go directly to the details page by pasting the full URL in the browser search bar or when refreshing the page, I encountered problems.

https://subdomain.michalweglarz.com/products/255/details

I would consistently get the default "404: Not found" Nginx error page with no indication as to what went wrong.

What I tried first

I went through all kinds of possible solutions found online.

None of these helped in the slightest. I would still encounter issues when entering dynamic routes, while the routes without any params worked just fine.

However, those various steps helped me narrow down the potential root cause of the issue. I tried to inspect NodeJS server logs, but nothing was recorded there at all. That turned out to be a crucial observation as it meant the app server didn't receive any problematic requests—they were captured at the earlier stage.

Having ensured all the application pieces worked correctly, I realized that it's Nginx itself that's causing issues.

The fix

I started with enabling debug logs in my site's Nginx config:

error_log /var/log/nginx/error.log debug;

This allowed me to see what exactly happens when I enter the route through client-side navigation and then when I refresh the page. It made this one error stand out in particular:

2025/11/11 19:07:07 [error] 3280075#3280075: *1391590 upstream sent too big header while reading response header from upstream, client: XXXX.XXXX.XXXX.XXXX, server: subdomain.michalweglarz.com, request: "GET /products/2155/details HTTP/1.1", upstream: "http://127.0.0.1:3000/products/2155/details", host: "subdomain.michalweglarz.com"

How come "upstream sent too big header while reading response header from upstream"? How big is "too big"?

I ran curl -I on the server to see the actual headers I receive. I used localhost:3000 to make sure I send a request to the target app container, without going through the Nginx proxy.

root@ubuntu-server:~/app# curl -I http://localhost:3000/products/2155/details
HTTP/1.1 200 OK
content-length: 54692
content-type: text/html
etag: "10xkh5w"
link: <../../_app/immutable/assets/PlatformBadge.DtRUKp-6.css>; rel="preload"; as="style"; nopush, <../../_app/immutable/assets/0.D8pq-Xe9.css>; rel="preload"; as="style"; nopush, <../../_app/immutable/assets/alert-dialog-footer.BUKjZNL4.css>; rel="preload"; as="style"; nopush, <../../_app/immutable/entry/start.WCUFboOm.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/C-ErDyVa.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/CgdMDdYd.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/CpgW9zns.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/FHpU5XBM.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/C5flx9JG.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/CddOdp6X.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/DWQ7Vghq.js>; rel="modulepreload"; nopush, <../../_app/immutable/entry/app.CNVZ7IZa.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/DsnmJJEf.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/BYxu1Dvs.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/HBvYjdq6.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/ZgB5n6C2.js>; rel="modulepreload"; nopush, <../../_app/immutable/nodes/0.COVp4aFM.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/Bxs0eZzK.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/DqMFxhzk.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/BZ3W4hMA.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/vZWZKW82.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/CqAc01qo.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/CbEuQmxg.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/Bb_Ic0Jo.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/__5RF9f-.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/A5LVLuVm.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/Dz1SAjNu.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/mXFomm73.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/C6oBmVHB.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/B1QSgM4O.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/BS8WaFYi.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/D0q4ZdKc.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/BWdIwgew.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/pWjUi97x.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/Msc_nn1I.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/BNxcICNy.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/DWRGcaF0.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/BQ4riPmK.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/gIKHm33p.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/TJXShw9Y.js>; rel="modulepreload"; nopush, <../../_app/immutable/nodes/5.GFbEMGjJ.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/CGHdhsKD.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/Cn0Jtv3T.js>; rel="modulepreload"; nopush, <../../_app/immutable/nodes/19.zyruwTaB.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/g6ydeHSj.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/DDd_F5Qk.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/DrPjDjGQ.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/RMgUUZSm.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/BfZnaqJV.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/BwllSzzP.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/CrTGsYKe.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/C4desBPv.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/ByJG7lNY.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/Bav3xWwu.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/C4A5_FMy.js>; rel="modulepreload"; nopush, <../../_app/immutable/chunks/Bq-uvKPs.js>; rel="modulepreload"; nopush
set-cookie: .AspNetCore.Cookies=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Strict; Secure
x-sveltekit-page: true
Date: Tue, 11 Nov 2025 19:14:05 GMT
Connection: keep-alive
Keep-Alive: timeout=5

There we go... This link header really is enormous. Turns out SvelteKit generates it automatically for preloading and prefetching resources. As the app grows in size and number of components, it can get very large and cause issues with reverse proxies, such as Nginx.

The easiest way to fix it in my case is to simply increase the allowed proxy buffer size in the Nginx config.

proxy_buffers 8 16k;
proxy_buffer_size 32k;
proxy_busy_buffers_size 64k;
large_client_header_buffers 4 16k;

Fortunately, this seems to do the trick and my app works correctly again!

An alternative solution suggested by Rich Harris in this thread could be applied directly in the SvelteKit userland, in hooks.server.ts file.

export function handle({ event, resolve }) {
	const response = await resolve(event);
	response.headers.delete('link');
	return response;
}

I'm glad I managed to dig through all this to find the root cause of the issue and eventually the solution, but I don't consider this time well spent. I feel this information should be included in the official SvelteKit docs, especially since this problem was acknowledged years ago.

Related GitHub discussions