nitrojs / nitro

Next Generation Server Toolkit. Create web servers with everything you need and deploy them wherever you prefer.
https://nitro.build
MIT License
6.24k stars 514 forks source link

cannot read form data with `readFormData` on vercel edge #1721

Closed pi0 closed 3 weeks ago

pi0 commented 1 year ago

Moving from https://github.com/unjs/nitro/discussions/1718 reported by @cosbgn

Minimal reproduction:

routes/index.ts:

export default defineEventHandler(async (event) => {
  if (event.method === "POST") {
    return await readFormData(event)
      .then((r) => r.get("name"))
      .catch((e) => e.stack);
  }
  return `
  <script type="module">
    console.log('Fetching...')
    const data = new FormData();
    data.append("name", "John Doe");
    const res = await fetch("/", { method: 'POST', body: data });
    const text = await res.text();
    console.log('Fetch complete: ' + text) 
    alert(text)
  </script>
  <h1>Check Console</h1>
`;
});

(checking console, response never handles)

Workaround

You can use the older readMultipartFormData utility. It returns an array of form datas:

export default defineEventHandler(async (event) => {
  if (event.method === "POST") {
    return await readMultipartFormData(event)
      .then((data) =>
        data.find((d) => d.name === "name")?.data.toString("utf8")
      )
      .catch((e) => e.stack);
  }
  return `
  <script type="module">
    console.log('Fetching...')
    const data = new FormData();
    data.append("name", "John Doe");
    const res = await fetch("/", { method: 'POST', body: data });
    const text = await res.text();
    console.log('Fetch complete: ' + text)
    alert(text)
  </script>
  <h1>Check Console</h1>
`;
});

https://nitro-ppspi2qf2-pi0.vercel.app/

passionate-bram commented 12 months ago

This also occurs on Netlify:

export default defineEventHandler(async event => {
  if (event.method !== "POST") {
    throw createError({statusCode:405, statusMessage: 'Method not allowed'});
  }
  let form: FormData;
  try {
    console.log('readFormData::enter');
    form = await readFormData(event);
    console.log('readFormData::exit');
  } catch (cause) {
    console.log('readFormData::error');
    throw createError({
      statusCode: 400,
      statusMessage: 'Request has no FormData',
      cause,
    });
  }
  return form;
});

Output (via Logs > Functions on Netlify's admin panel)

Nov 20, 02:46:34 PM: INIT_START Runtime Version: nodejs:18.v18  Runtime Version ARN: arn:aws:lambda:us-east-2::runtime:(snip)
Nov 20, 02:46:34 PM: (snip) INFO   readFormData::enter
Nov 20, 02:46:34 PM: (snip) Duration: 39.99 ms  Memory Usage: 78 MB Init Duration: 215.70 ms    

The result is a 502 with message:

error decoding lambda response: invalid status code returned from lambda: 0

There are two major issues here:

  1. The catch clause is not triggered, leading me to believe the entire process crashed.
  2. By no means can the error be caught.

With this severity of a crash, I'd honestly prefer the readFormData function be replaced with an always throwing function. That would not change the absence of it's functionality and you would gain back the ability to recover from the failure. Also, the developer(s) do not need to go around in circles trying out different small changes to their code to hunt this bug down.

passionate-bram commented 12 months ago

Using console.log(event), the event.node.req.body property holds the actual body of the request already.

Reviewing the code for h3, specifically src/utils/body.ts, it seems there are wildly different ways of parsing the request. I wouldn't be surprised if some of the functions there would actually be able to parse the request properly.

For example, readRawBody does reach for the event.node.req.body property. While the readFormData does not. I'll see if I can find a variant that works on netlify and then the toWebRequest or readFormData may be modified to use that approach as well.

passionate-bram commented 12 months ago

This is a fixed version of readFormData that will work on netlify, and perhaps also vercel (given that it is also AWS based):

async function readFormData_fixed(event: H3Event) : Promise<FormData> {
  const request = new Request(getRequestURL(event), {
    // @ts-ignore Undici option
    duplex: 'half',
    method: event.method,
    headers: event.headers,
    body: await readRawBody(event),
  } as RequestInit);
  return await request.formData();
}
besscroft commented 11 months ago

I ran into a similar problem with Netlify: https://github.com/unjs/h3/issues/590

fezproof commented 10 months ago

Having this problem in solid start as well. Appears on cloudflare workers, netlify edge and vercel edge

AirBorne04 commented 9 months ago

Hi,

I have been having the same issue in solid start on the cloudflare env.

Accessing the event.web.request or event._requestBody it can't be read. Instead wrangler is generating an error stating A hanging Promise was canceled.

The only request object that can be used is the event.node.req.body.

pi0 commented 9 months ago

@AirBorne04 sadly we don't officially support Solid as they diverged. Sorry for you hearing this. I advice you to seek support in their channel.

AirBorne04 commented 9 months ago

no problem @pi0 the PR for fixing it on the Solid side is already merged.

pi0 commented 3 weeks ago

This issue is already fixed with https://github.com/unjs/h3/pull/616 (h3@1.10.1)

I have implemented an end-to-end test which passes on edge platforms:

Note: Tests are against unmodified Nitro behavior.