withastro / astro

The web framework for content-driven websites. ⭐️ Star to support our work!
https://astro.build
Other
44.69k stars 2.33k forks source link

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

Closed rafaellucio closed 3 months ago

rafaellucio commented 3 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

Participation

matthewp commented 3 months ago

If you remove simple-stack-stream do you still have the issue?

rafaellucio commented 3 months ago

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

I don't use simple-stack-stream and receive the similar error when submit data

And I remove simple-stack-stream

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),
      ),
    }),
  ],
});

But receive de same error 😢

image

rafaellucio commented 3 months ago

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

....


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

rafaellucio commented 3 months ago

@matthewp I identified the problem, when this action-hosting-deploy use the CLI to create an api function in Google Cloud Function this function use an library called firebase-frameworks, this library haven't support to application/x-www-form-urlencoded, when Astro run context.request.formData() the formData don't exists.

Related issue

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 😢

I look this library firebase-framework-tools/astro maybe can create an Github Actions, and use CLI to publish function using this library

matthewp commented 3 months ago

Ah ok, x-www-form-urlencoded is not the right content type for FormData. Instead you want to use URLSearchParams like so let params = new URLSearchParams(await request.text()).

orkisz commented 2 months ago

@rafaellucio Please consider getting rid of firebase-frameworks at all. I tried many ways and always something doesn't work as expected, especially environment variables and form data.

My suggestion: create Dockerfile as described here: https://docs.astro.build/en/recipes/docker/#ssr, deploy to Google Cloud Run and don't use Google Functions at all. If you don't believe me, go and take a look at your functions' source code - firebase-frameworks has made a nice spaghetti out of it.

porfidev commented 1 month ago

as @orkisz mentioned it, better try external service.

I change my forms instead use x-www-form... change it to json/application but on image processing that does not work.

 const data = await request.formData();
  const file = data.get("file") as File;
  const promoName = data.get("name");

  if (!file) {
    return new Response(JSON.stringify({ error: "No file uploaded" }), {
      status: 400,
      headers: { "Content-Type": "application/json" },
    });
  }

  const auth = getAuth(app);
  const sessionCookie = cookies.get("__session")?.value || '';
  const authorized = await auth.verifySessionCookie(sessionCookie);

  if(!authorized) {
    return new Response(
      JSON.stringify({
        message: "Cookie No valida!",
      }),
      { status: 403 }
    );
  }

  const imageReader = file.stream().getReader();
  let receivedLength = 0;
  let chunks = [];

  while (true) {
    const { done, value } = await imageReader.read();
    if (done) {
      break;
    }
    chunks.push(value);
    receivedLength += value.length;
  }

  const combinedChunks = Buffer.concat(chunks, receivedLength);
  const newFileName = new Date().getMilliseconds() + file.name;
  await storage.bucket('orthodent-center.appspot.com')
    .file('promos/' + newFileName)
    .save(combinedChunks);

waste of time, works locally but does not work at all on Firebase Servers