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

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

Open
rafaellucio opened this issue Apr 25, 2024 · 3 comments

Comments

@rafaellucio
Copy link

rafaellucio commented Apr 25, 2024

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
withastro/astro#10870

@google-oss-bot
Copy link
Contributor

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

@aalej
Copy link
Contributor

aalej commented Apr 26, 2024

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

rafaellucio commented Apr 27, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants