firebase / firebase-tools

The Firebase Command Line Tools
MIT License
3.98k stars 918 forks source link

@astro/node gcloud [ERROR] TypeError: Error: Unexpected end of multipart data #7046

Open rafaellucio opened 2 months ago

rafaellucio commented 2 months ago

Astro Info

Astro                    v4.5.17
Node                     v20.0.0
System                   macOS (arm64)
Package Manager          npm
Output                   server
Adapter                  @astrojs/node
Integrations             @astrojs/react
                         @astrojs/tailwind
                         simple-stream

If this issue only occurs in one browser, which browser is a problem?

No response

Describe the Bug

I receibe this message when I deploy my app [ERROR] TypeError: Error: Unexpected end of multipart data, following this documentation https://docs.astro.build/en/recipes/build-forms-api/#recipe

Today I use the standalone node setup in astro.config.mjs

import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwind from '@astrojs/tailwind';
import node from '@astrojs/node';
import { fileURLToPath } from 'node:url';

import simpleStackStream from "simple-stack-stream";

// https://astro.build/config
export default defineConfig({
  output: 'server',
  adapter: node({
    mode: 'standalone'
  }),
  outDir: './dist',
  integrations: [react(), tailwind({
    configFile: fileURLToPath(new URL('./tailwind.config.mjs', import.meta.url))
  }), simpleStackStream()]
});

This is my API test

// my file /api/login.ts
import type { APIContext } from 'astro';

export async function POST({ request }: APIContext) {
  const formData = await request.formData();

  return new Response(
    JSON.stringify({
      email: formData.get('email'),
      password: formData.get('password'),
    }),
  );
}

export async function GET({ request }: APIContext) {
  return new Response(
    JSON.stringify({
      name: 'Astro',
      url: 'https://astro.build/',
    }),
  );
}

After call GET this works, but post doesn't works

GET /ssrdineramainsights/api/login

❯ curl --location 'https://us-central1-dinerama-2912c.cloudfunctions.net/ssrdineramainsights/api/login'

{"name":"Astro","url":"https://astro.build/"}

POST /ssrdineramainsights/api/login

curl --location 'https://us-central1-dinerama-2912c.cloudfunctions.net/ssrdineramainsights/api/login' \
-F email="my@domain.com" \
-F password="pass123" -v

* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* h2h3 [:method: POST]
* h2h3 [:path: /ssrdineramainsights/api/login]
* h2h3 [:scheme: https]
* h2h3 [user-agent: curl/7.84.0]
* h2h3 [accept: */*]
* h2h3 [content-length: 257]
* h2h3 [content-type: multipart/form-data; boundary=------------------------03196803d06dcd43]
* Using Stream ID: 1 (easy handle 0x12f811800)
> POST /ssrdineramainsights/api/login HTTP/2
> user-agent: curl/7.84.0
> accept: */*
> content-length: 257
> content-type: multipart/form-data; boundary=------------------------03196803d06dcd43
>
* We are completely uploaded and fine
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/2 500
< x-cloud-trace-context: 077bcf6d7925f41efa7d4329bcc97534;o=1
< date: Wed, 24 Apr 2024 14:34:18 GMT
< content-type: text/html
< server: Google Frontend
< content-length: 0
< alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
<
* Connection #0 to host us-central1-dinerama-2912c.cloudfunctions.net left intact

The log em Google Cloud Function is:

[ERROR] TypeError: Error: Unexpected end of multipart data

Screenshot 2024-04-24 at 11 50 28

What's the expected result?

{
  email: 'my@domain.com',
  password: 'pass123',
}

Link to Minimal Reproducible Example

https://stackblitz.com/edit/github-ao77yt?file=src%2Fpages%2Findex.astro,src%2Fpages%2Fapi%2Flogin.ts,package.json

This error is very very strange, because when I run build and run locally

❯ node dist/server/entry.mjs

Works!!, But after deploy doesn't works

I use this actions to deploy my app in gcloud https://github.com/FirebaseExtended/action-hosting-deploy

Another same issue https://github.com/withastro/astro/issues/10870

google-oss-bot commented 2 months ago

This issue does not seem to follow the issue template. Make sure you provide all the required information.

aalej commented 2 months ago

Hey @rafaellucio, thanks for the detailed report. I was able to reproduce the issue you mentioned. Just to note, one thing I tried is to create a simple POST api and I was able to get it working:

// api/test.ts
import type { APIContext } from "astro";

export async function POST({ request }: APIContext) {
 console.log(" --- api/test POST --- ");
 const body = await request.json();
 return new Response(
   JSON.stringify({
     email: body.email,
     password: body.password,
     status: "ok",
   })
 );
}

export async function GET({ request }: APIContext) {
 return new Response(
   JSON.stringify({
     name: "Astro",
     url: "https://astro.build/",
   })
 );
}

It seems like the error starts occurring on const formData = await request.formData();.

Let me raise this to our engineering team so they can take a look. I created this mcve using the information provided.

rafaellucio commented 2 months ago

Yes @aalej, my intent is use default behavior in Astro when I implemente an formulary

Basically add a simple form like this:

<form action="/api/test" method="POST">
  <input name="user" />
  <input name="pass" />
  <button>submit</button>
</form>

When I use Astro the content type of the request context receives an application/x-www-form-urlencoded and to obtain these values ​​I need to parse my request to FormData using request.formData() as request .json ( ) and added a new method in the request context called formData

My workarround was create an Astro middleware and use the busboy library to obtain values from request, like this

import { defineMiddleware } from 'astro:middleware';
import Busboy from 'busboy';

const getFieldsFromFormData = (headers: any, body: any) =>
  new Promise(async (resolve) => {
    const busboy = Busboy({ headers });
    let fields: any = {};

    busboy.on('field', (field: string, val: any) => {
      fields = JSON.parse(field);
    });
    busboy.on('finish', () => resolve(fields));
    busboy.end(body);
  });

export const formBody = defineMiddleware(async (context, next) => {
  const req = context.request.clone();
  const headers = Object.fromEntries(req.headers);

  if (
    req.method === 'POST' &&
    req.headers.get('content-type') === 'application/x-www-form-urlencoded'
  ) {
    try {
      const text = await req.text();
      const fields: any = await getFieldsFromFormData(headers, text);

      context.request.formData = async function () {
        return {
          ...fields,
          get: (key: string) => fields[key] ?? '',
        };
      };
    } catch (err) {
      console.error(err);
    }
  }
  return next();
});

This works but it's a really bad solution 😢

Maybe this behavior can be add here firebase-frameworks-tools/astro