vercel / next.js

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

`Request` object behaves differently in fetch #66840

Open lennartkerkvliet opened 1 week ago

lennartkerkvliet commented 1 week ago

Link to the code that reproduces this issue

https://github.com/lennartkerkvliet/next-fetch-request-bug

To Reproduce

When sending a request body using the Request object in a Next.js application while using server actions, the content-length parameter is not included in the request and instead chunked transfer encoding is used. This behavior differs from the expected functionality of the Fetch API, which internally is also constructing a Request object.

Steps to reproduce the behavior:

  1. Create a Request object with a body.
  2. Pass the Request object to the fetch function.

Example code:

const body = JSON.stringify({ username: "test", password: "password" });
const request = new Request("https://www.my-site.com/api/login", { 
    method: "POST", 
    headers: { "Content-Type": "application/json" },
    body: body
});
const response = await fetch(request); // Server receives an empty body

Current vs. Expected behavior

I would expect the parameters in Request to be identical to when using the more traditional use of fetch API:

const body = JSON.stringify({ username: "test", password: "password" });
const response = await fetch("https://www.my-site.com/api/login", { 
    method: "POST", 
    headers: { "Content-Type": "application/json" },
    body: body
});

but instead will return the latter

Screenshot 2024-06-13 at 18 27 51

Provide environment information

Operaring System:
  Arch: arm64
  Version: macOS Sonoma 14.4.1

Binaries:
  Node: v22.2.0
  npm: 10.7.0

Relevant Packages:
  next: 14.2.3

Which area(s) are affected? (Select all that apply)

Not sure, Runtime

Which stage(s) are affected? (Select all that apply)

next dev (local), Vercel (Deployed), Other (Deployed)

Additional context

It seems to be somewhat related to the changes NextJS is making to the Response object and in particular the duplex option. The Django server I am using for my own project does not support transfer-encoding, and will mark the request as a "Bad request syntax".

icyJoseph commented 1 week ago

I can't reproduce with 14.2.4 and node 22.1.0.

I used a request bin: https://public.requestbin.com/r/enhaa8qwmvxi7/2hpuXZ1mqrzZs3xYQrlXRux6jny

And as you can see, content length was always respected, when using:

export default async function Home() {
  const body = JSON.stringify({ information: "test-abc" });
  const url = "https://enhaa8qwmvxi7.x.pipedream.net";
  const options = {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: body,
  };

  // await fetch(url, options);
  await fetch(new Request(url, options));

  return <div>Hi</div>;
}

I also tested with 14.2.3 - does it have to be within a server action?

icyJoseph commented 1 week ago

Indeed, it has to be using a server action...

Image

MahadAdil commented 1 week ago

Hi there, Apparently reproducing your code throws the following error:

Error: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.

This is because in Next.js in order to protect client-server boundary, Next.js restricts passing Non-serializable value from client to server and since Request() object is non-serializable your code throws an error. Upon refactoring your code within "use server" block, everything works as expected and the server returns status 200 upon POST request.

Working Code:

const body = JSON.stringify({ username: "test", password: "password" });

async function brokenAction() {
    "use server"

    // Refactored the Request Object within "use-server" scope
    const request = new Request("https://www.my-site.com/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: body
    });

    // this will *not* include the body in the request
    await fetch(request)
  }

Kindly let me know if this helps or if I missed anything.

lennartkerkvliet commented 1 week ago

@icyJoseph You're correct. When rewriting, I forgot to add the context that this only happens in the server environment, which is where NodeJS Undici is being used. This behavior is internally changed by next and that seems to be causing this issue.

lennartkerkvliet commented 1 week ago

@MahadAdil Did you check out the included sample code? Route requests (in the App Router) do allow chunked encoding so unlike my custom server it's not broken, but you can see that the two server actions which should send the same Request, send different ones. The screenshot of the diff in my description comes from that sample code as well.

icyJoseph commented 1 week ago

@lennartkerkvliet not quite, you see, it also works fine, if you make a request as you render, in the server. At least from what I remember testing yesterday.

The issue happens, when the request is made within a server action, not exactly the same thing as above.

lennartkerkvliet commented 1 week ago

@icyJoseph you mean it works fine when using SSR but not when using Server Actions? I didn't test it, but it would make sense as the patch applied by next only happens in the server action environment AFAIK.