Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
1 task done
DamianEdwards opened this issue Jun 21, 2023 · 14 comments
Open
1 task done

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

DamianEdwards opened this issue Jun 21, 2023 · 14 comments
Labels
bug Issue was opened via the bug report template. Output (export/standalone) Related to the the output option in `next.config.js`. Runtime Related to Node.js or Edge Runtime with Next.js.

Comments

@DamianEdwards
Copy link

Verify canary release

  • I verified that the issue exists in the latest Next.js 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

@DamianEdwards DamianEdwards added the bug Issue was opened via the bug report template. label Jun 21, 2023
@github-actions github-actions bot added Runtime Related to Node.js or Edge Runtime with Next.js. Output (export/standalone) Related to the the output option in `next.config.js`. labels Jun 21, 2023
@flux0uz
Copy link

flux0uz commented Jun 21, 2023

Perhaps it's related with this issue ? 51605

@NadhifRadityo
Copy link

NadhifRadityo commented Jun 21, 2023

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
Copy link

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
Copy link
Author

@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
Copy link

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.)
    if (opts.keepAliveTimeout) {
    server.keepAliveTimeout = opts.keepAliveTimeout
    }
  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.

const invokeReq = http.request(
parsedTargetUrl.toString(),
{
headers: invokeHeaders,
method: requestInit.method,
},

@NadhifRadityo
Copy link

NadhifRadityo commented Aug 12, 2023

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:

invokeRes = await invokeRequest(
renderUrl,
{
headers: invokeHeaders,
method: req.method,
signal: signalFromNodeResponse(res),
},
getRequestMeta(req, '__NEXT_CLONABLE_BODY')?.cloneBodyStream()
)
return await fetch(parsedTargetUrl.toString(), {
headers: invokeHeaders as any as Headers,
method: requestInit.method,
redirect: 'manual',
signal: requestInit.signal,
...(requestInit.method !== 'GET' &&
requestInit.method !== 'HEAD' &&
readableBody
? {
body: readableBody as BodyInit,
duplex: 'half',
}
: {}),
next: {
// @ts-ignore
internal: true,
},
})

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

@ugurrdemirel
Copy link

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
Copy link

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
Copy link

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
Copy link

autsada commented Oct 9, 2023

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
Copy link

@autsada had a similar issue

@PassionPenguin
Copy link

PassionPenguin commented Nov 14, 2023

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

  • using next.js 14.0.2
  • node 18.17.0 and 21.1.0
  • npm 10.2.1

 ⨯ 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
Copy link
Member

vvo commented Nov 15, 2023

@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
Copy link

@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!!!!

kodiakhq bot pushed a commit that referenced this issue Nov 29, 2023
…handlers (#58885)

### What?
Calling `redirect` or `permanentRedirect` with a route handler used by a server action will result in that POST request following the redirect. This could result in unexpected behavior, such as re-submitting an action (in the case where the redirected URL makes use of the same server action).

### Why?
By spec, 307 and 308 status codes will attempt to reuse the original request method & body on the redirected URL.

### How?
In all cases when calling a `redirect` handler inside of an action, we'll return a `303 See Other` response which is a typical status code when redirecting to a success / confirmation page as a result of a POST/PUT.

The other option would be to use 301 / 302 status codes, but since we're already doing a 303 status code [here](https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/action-handler.ts#L603), this aligns the behavior for the route handler case. 

Closes NEXT-1733
See also: #51592 (comment)
[Slack x-ref](https://vercel.slack.com/archives/C03S8ED1DKM/p1700060786749079)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Issue was opened via the bug report template. Output (export/standalone) Related to the the output option in `next.config.js`. Runtime Related to Node.js or Edge Runtime with Next.js.
Projects
None yet
Development

No branches or pull requests

8 participants