vercel / next.js

The React Framework
https://nextjs.org
MIT License
125.69k stars 26.84k forks source link

Next.js server returns `connection: close` header for all routes #51592

Open DamianEdwards opened 1 year ago

DamianEdwards commented 1 year ago

Verify canary release

Provide environment information

Operating System:
      Platform: darwin
      Arch: arm64
      Version: Darwin Kernel Version 22.1.0: Sun Oct  9 20:14:30 PDT 2022; root:xnu-8792.41.9~2/RELEASE_ARM64_T8103
    Binaries:
      Node: 20.3.0
      npm: 9.6.7
      Yarn: 1.22.19
      pnpm: N/A
    Relevant packages:
      next: 13.4.7-canary.2
      eslint-config-next: 13.4.6
      react: 18.2.0
      react-dom: 18.2.0
      typescript: 5.1.3

Which area(s) of Next.js are affected? (leave empty if unsure)

Middleware / Edge (API routes, runtime), Standalone mode (output: "standalone")

Link to the code that reproduces this issue or a replay of the bug

npx create-next-app

To Reproduce

  1. Create a new app using npx create-next-app@latest
  2. Run the app using npx next dev
  3. Launch a browser and open the Network tab in the developer tools
  4. Navigate to http://localhost:3000
  5. In the Network tab, select the request for localhost and note that in the Response headers connection: close is listed. Select other requests and note the same header is sent for all responses.
  6. Back in the terminal, Ctrl+C to stop the dev server
  7. Run npx next build to build the app, then run npx next start to run it
  8. Back in the browser, refresh the page and observe that the same behavior is present once the app is built

Describe the Bug

The nodejs server created by Next.js, whether it be in dev mode, production mode after building, or production mode when standalone deployment is configured, returns a connection: close response header to clients when rendering routes. This instructs the client to close the underlying TCP connection, rather than returning connection: keep-alive which allows the client to reuse the TCP connection when making subsequent requests, greatly reducing latency. This behavior is leading to high latencies for server-rendered Next.js routes.

I reproduced this behavior on multiple machines, including OSX and Windows 11, on multiple versions of nodejs, including latest LTS and Current releases, using next 13.4.x and canary, with multiple browsers including Edge and Firefox.

Expected Behavior

The nodejs server created by Next.js should utilize HTTP 1.1 keep-alive to allow clients to keep TCP connections open.

Which browser are you using? (if relevant)

Edge, Firefox

How are you deploying your application? (if relevant)

next start

flux0uz commented 1 year ago

Perhaps it's related with this issue ? 51605

NadhifRadityo commented 1 year ago

This is happening because of how Nextjs handles incoming packets. By default, Nextjs will start two HTTP servers. One server sits in the main thread that acts like a proxy, and another thread sits in a forked mode, where your main app will be running (See start-server.ts#L240).

I think the devs structured this way to avoid memory leaks. If the memory usage on the forked thread exceeds a certain threshold, the main thread simply restarts the forked thread without killing the main HTTP server.

Now, back to the main question. Why does it put Connection: close to the header? The problem is how the packets are relayed to the forked thread. Nextjs uses a library called http-proxy (See start-server.ts#L255) which just proxies incoming request and relays it to another server (in this case the other HTTP server that sits on forked thread). ~Internally the library doesn't handle keepalive connections. Thus, it appends Connection: close to the header.~ Edit: Nope, the library does support keep-alive connection under some conditions (See node-http-proxy/.../web-outgoing.js#L46).

An alternative solution would be turning off the worker thread, but the devs have not given that an option yet (See next-dev.ts#L220). Typically, Nextjs only handles web server and should not handle anything else, as it will create more complexities on the project. Personally, I would just put a reverse proxy (like nginx) in front of Nextjs server. Thus, making closed TCP connection not much of a big deal.

NadhifRadityo commented 1 year ago

Perhaps it's related with this issue ? 51605

I read the stack trace and it starts with TLSSocket. And if I am not mistaken, Nextjs doesn't handle TLS/SSL. Furthermore, referring to my former comment, when the main thread relays the request to the forked thread, it uses plain old HTTP.

DamianEdwards commented 1 year ago

@NadhifRadityo it's not clear (at least to me) from the code you link to why that's resulting in a connection: close header. To my understanding, Node will only respond with connection: close if keep-alive is explicitly disabled by setting server.keepAliveTimeout to 0. The keep-alive timeout is accepted as a parameter that defaults to null and is only set on the server if it's not null, so 0 would need to be passed in, or some other part of the server logic is causing the issue.

NadhifRadityo commented 1 year ago

After further digging into the code, I have a small working patch to make the server utilizes keep-alive connections.

Edit the file node_modules/next/dist/server/lib/start-server.js starting at line 221 to the following patch:

const proxyServer = httpProxy.createProxy({
    target: targetUrl,
    changeOrigin: false,
    ignorePath: true,
    xfwd: true,
    ws: true,
    followRedirects: false,
+    agent: new (require("http")).Agent({
+        keepAlive: true
+    })
});

Explanation: Apparently, my former comment was partially correct. After a bit of experimenting, I realized:

  1. The forked thread did not put Connection: close to the response header (and server.keepAliveTimeout is the node's default value by the documentation you mentioned.) https://github.com/vercel/next.js/blob/57ab2818b93627e91c937a130fb56a36c41629c3/packages/next/src/server/lib/render-server.ts#L81-L83
  2. The proxied response already has Connection: close. Now this is a bit weird. Because the original request specified Connection: keep-alive, but the server responded otherwise.
  3. The http-proxy library explicitly sets Connection: close to the proxied request. This explains why the server responded Connection: close on point 2. https://github.com/http-party/node-http-proxy/blob/9b96cd725127a024dabebec6c7ea8c807272223d/lib/http-proxy/common.js#L62-L74

By setting a custom agent, http-proxy will use that agent to manage live TCP connections. Currently, a manual patch is required, since Nextjs don't have options to manage http-proxy yet.

Note: The same patch probably also applies to IPC communications between Next's router and Next's actual page renderer. https://github.com/vercel/next.js/blob/57ab2818b93627e91c937a130fb56a36c41629c3/packages/next/src/server/lib/server-ipc/invoke-request.ts#L26-L31

NadhifRadityo commented 1 year ago

I noticed Next.js changed its request proxying recently. Currently, Next.js uses built-in fetch instead of http-proxy for basic HTTP request. These changes made the Connection header as-is, as opposed to the http-proxy which overrides the header.

In the latest canary version, Connection header already defaulted to keep-alive with timeout of 5 seconds (which is the default nodejs http keep-alive timeout). For reference, please see the following snippets: https://github.com/vercel/next.js/blob/2b38118ea005c1bf6bc4059534754080cef7af54/packages/next/src/server/lib/router-server.ts#L368-L376 https://github.com/vercel/next.js/blob/2b38118ea005c1bf6bc4059534754080cef7af54/packages/next/src/server/lib/server-ipc/invoke-request.ts#L27-L46

If you think the problem no longer exists, please consider closing this issue. Thank you!

ugurrdemirel commented 1 year ago

I had same problem with docker based on node:20-bookworm image and tried node:20-alpine as well. When I changed to node:19, it fixed the issue. Hope this helps

NadhifRadityo commented 1 year ago

I had same problem with docker based on node:20-bookworm image and tried node:20-alpine as well. When I changed to node:19, it fixed the issue. Hope this helps

I think there is (or should be) NO correlation between node version 19/20 that made the connection header keep-alive by itself. Since the behaviour is based on Next.js' code handling request. Can you confirm that you were running the same Next.js version?

Though, this issue is actually been resolved by the latest Next.js version, as per my last message.

ugurrdemirel commented 1 year ago

I think there is (or should be) NO correlation between node version 19/20 that made the connection header keep-alive by itself. Since the behaviour is based on Next.js' code handling request. Can you confirm that you were running the same Next.js version?

Though, this issue is actually been resolved by the latest Next.js version, as per my last message.

I was facing this issue on next@13.4.12. I tried to install latest nextjs version which was next@13.4.18. I removed both .next & node_modules folders from my project to avoid any cache or something and I build the project again. The result was same. Then I've become the man who hits every key to pass the level and tried to change nodejs version from docker and it fixed it.

autsada commented 1 year ago

I am facing this error Error: Connection closed. at c on one specific route when deployed to Vercel. This route works fine in the local build. I used next@13.5.4 and also tried to downgrade to several versions, but none worked.

ColeTownsend commented 11 months ago

@autsada had a similar issue

PassionPenguin commented 11 months ago

facing a similar issue... using api endpoints with redirect causes application to throw Error: Connection closed.


 ⨯ Error: Connection closed.
    at tu (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:35:303532)
    at t.decodeReply (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:35:304915)
    at /Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:38:264
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async tX (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:37:5207)
    at async rl (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:38:22994)
    at async doRender (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/server/base-server.js:1407:30)
    at async cacheEntry.responseCache.get.routeKind (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/server/base-server.js:1561:40)
    at async DevServer.renderToResponseWithComponentsImpl (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/server/base-server.js:1479:28)
    at async DevServer.renderPageComponent (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/server/base-server.js:1850:24)
    at async DevServer.renderToResponseImpl (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/server/base-server.js:1888:32)
    at async DevServer.pipeImpl (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/server/base-server.js:902:25)
    at async NextNodeServer.handleCatchallRenderRequest (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/server/next-server.js:266:17)
    at async DevServer.handleRequestImpl (/Users/hoarfroster/Desktop/hfrecipe/node_modules/next/dist/server/base-server.js:798:17) 

export async function POST(req: Request) {
...some logic

  let tmp = new URL(path, req.url);
  tmp.searchParams.set("updated", "true");
  return NextResponse.redirect(tmp);

}

the api page acted perfectly, but when redirected, the issue throws.(refreshing the redirected page, it shows normally)
vvo commented 11 months ago

@PassionPenguin You can use:

Response.redirect(`${request.nextUrl.origin}/path`, 303);

to solve your issue. I bet you were using redirect() in a POST route handler? If that's the case then you can use the workaround of Response.redirect.

The Next.js team will have to fix redirect() probably. Repository: https://github.com/vvo/next-redirect-connection-closed Live demo: https://next-redirect-connection-closed.vercel.app/ (error codes are 405 on vercel and 500 in dev)

It turns out any <form method="POST"> to a non-existent path will also result in 500 (probably because POST to the error page is the same as POST to a page).

PassionPenguin commented 11 months ago

@PassionPenguin You can use:

Response.redirect(`${request.nextUrl.origin}/path`, 303);

to solve your issue. I bet you were using redirect() in a POST route handler? If that's the case then you can use the workaround of Response.redirect.

The Next.js team will have to fix redirect() probably. Repository: https://github.com/vvo/next-redirect-connection-closed Live demo: https://next-redirect-connection-closed.vercel.app/ (error codes are 405 on vercel and 500 in dev)

It turns out any <form method="POST"> to a non-existent path will also result in 500 (probably because POST to the error page is the same as POST to a page).

yes u're correct! thx!!!!