fabian-hiller / modular-forms

The modular and type-safe form library for SolidJS, Qwik and Preact
https://modularforms.dev
MIT License
1.05k stars 55 forks source link

Internal server error: The body has already been consumed. #142

Closed cwoolum closed 6 months ago

cwoolum commented 1 year ago

Hello,

I recently just started running into a strange issue with my form. When I try to save, I now receive the following error

7:49:50 AM [vite] Internal server error: The body has already been consumed.
      at consumeBody (node:internal/deps/undici/undici:6496:19)
      at consumeBody.next (<anonymous>)
      at Request.formData (node:internal/deps/undici/undici:6585:32)        
      at Object.formActionQrl_globalActionQrl_VOLgKQUpNuc (C:\proj\admin\node_modules\@modular-forms\qwik\dist\index.qwik.mjs:848:139)
      at Object.invoke (C:\proj\dist-dev\tsc-out\packages\qwik\src\core\util\implicit_dollar.js:1303:26)
      at C:\proj\dist-dev\tsc-out\packages\qwik\src\core\util\implicit_dollar.js:8286:35
      at then (C:\proj\dist-dev\tsc-out\packages\qwik\src\core\util\implicit_dollar.js:446:56)
      at C:\proj\dist-dev\tsc-out\packages\qwik\src\core\util\implicit_dollar.js:8272:20
      at Object.invokeQRL (C:\proj\dist-dev\tsc-out\packages\qwik\src\core\util\implicit_dollar.js:8305:30)
      at C:\proj\node_modules\@builder.io\qwik-city\vite\index.cjs:24446:34

The line with the error is

const values = type === "application/x-www-form-urlencoded" || type === "multipart/form-data" ? getFormDataValues(await event.request.formData(), formDataInfo) : jsonData;

I use Valibot for validation but this error also seems to occur if I remove validation. My formAction$ is never hit either so this seems to be something happening when the form is submitted and but before my own logic is run.

Here is my form configuration

import { $, component$ } from "@builder.io/qwik";
import { routeLoader$, server$ } from "@builder.io/qwik-city";
import { BarService } from "~/services/bar-service";
import { Client } from "@googlemaps/google-maps-services-js";
import {
  formAction$,
  insert,
  remove,
  required,
  useForm,
  valiForm$,
  type InitialValues,
} from "@modular-forms/qwik";
import { Response } from "~/components/input/response";
import Button from "~/components/primitives/button";
import { FileInput } from "~/components/input/file-input";
import { Select } from "~/components/input/select";
import { TextInput } from "~/components/input/text-input";
import { AzureStorageService } from "~/services/storage";
import { formatAddress } from "~/util/address";
import { BarFormSchema, type BarFormType } from "~/util/schemas/bar";
import zipcodes from "zipcodes";

const client = new Client({});

// Define the route action for submitting the form.
export const useSubmitBarForm = formAction$<BarFormType>(
  async (formData, { params, env, redirect }) => {

    console.log("Submitting");

    let barId = params.barId;

    const barService = new BarService(env);

    if (barId) {
      await barService.updateBar(barId, formData as any);
    } else {
      barId = await barService.createBar(formData);
    }

    throw redirect(302, "/bars");
  },
  {
    validate: valiForm$(BarFormSchema),
    numbers: [
      "location.x",
      "location.x",
      "venueCapacity",
      "tvDetails.averageTvSize",
      "tvDetails.indoorTvs",
      "tvDetails.largestTvSize",
      "tvDetails.outdoorTvs",
      "tvDetails.totalTvs",
    ],
    arrays: ["hours", "images", "drinks"],
    files: ["images.$.file", "availableChannelsIds"],
    dates: [],
  }
);

export const useFormLoader = routeLoader$<InitialValues<BarFormType>>(
  async (requestEvent) => {
    const barId = requestEvent.params.barId;

    const barService = new BarService(requestEvent.env);

    const bar = await barService.getBar(barId);

    if (!bar.hours) {
      bar.hours = [];
    }

    const mappedBar: BarFormType = {
      ...bar,
      images: [],
      location: {
        address1: bar.location.address1,
        address2: bar.location.address2 ?? "",
        cityName: bar.location.cityName,
        stateCode: bar.location.stateCode,
        postalCode: bar.location.postalCode,
        x: bar.location.coordinates.coordinates[0],
        y: bar.location.coordinates.coordinates[1],
      },
    };

    return mappedBar as any;
  }
);

// Define the BarForm component.
export default component$(() => {
  const bar = useFormLoader();

  // Get the action store for the form submission.
  const submitBarForm = useSubmitBarForm();

  const [barForm, { Form, Field, FieldArray }] = useForm<BarFormType>({
    loader: bar,
    action: submitBarForm,
    fieldArrays: ["hours", "images"],
  });

  const updateFormAddressInfo = $((zipCode: zipcodes.ZipCode) => {
    const fields = barForm.internal.fields;

    if (fields["location.cityName"]) {
      fields["location.cityName"].value = zipCode.city;
    }

    if (fields["location.stateCode"]) {
      fields["location.stateCode"].value = zipCode.state;
    }
  });

  return (
    <>
      <Form
        class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
        encType="multipart/form-data"
      >
        <h4 class="text-2xl dark:text-white mb-2">General Information</h4>

        <div>{barForm.response.message}</div>

        <Field name="name" validate={[required("Name is required")]}>
          {(field: { error: any; value: any }, props) => (
            <TextInput
              {...props}
              label="Name"
              required="true"
              error={field.error}
              type="text"
              value={field.value}
            />
          )}
        </Field>

        <Field name="foodType">
          {(field: { error: any; value: any }, props) => (
            <TextInput
              {...props}
              label="Food type"
              error={field.error}
              type="text"
              value={field.value}
            />
          )}
        </Field>

        <Field name="venueCapacity" type="number">
          {(field: { error: any; value: any }, props) => (
            <TextInput
              {...props}
              label="Venue Capacity"
              error={field.error}
              type="number"
              value={field.value}
            />
          )}
        </Field>

        <h4 class="text-2xl dark:text-white mb-2">Location</h4>

        <div class="row">
          <Field
            name="location.address1"
            validate={[required("Address is required")]}
          >
            {(field: { error: any; value: any }, props) => (
              <TextInput
                {...props}
                wrapperClass="col-md-6"
                label="Address 1"
                required="true"
                error={field.error}
                type="text"
                value={field.value}
              />
            )}
          </Field>

          <Field name="location.address2">
            {(field: { value: any }, props) => (
              <TextInput
                {...props}
                label="Address 2"
                wrapperClass="col-md-6"
                type="text"
                value={field.value}
              />
            )}
          </Field>
        </div>

        <div class="row">
          <Field name="location.postalCode">
            {(field: { error: any; value: any }, props) => (
              <TextInput
                {...props}
                label="Zip Code"
                required="true"
                wrapperClass="col-md-2"
                error={field.error}
                type="text"
                onInput$={(evt: any) => {
                  console.log("Looking");
                  if (!evt.target?.value) {
                    return;
                  }

                  const zipCodeInfo = zipcodes.lookup(evt.target.value);

                  if (zipCodeInfo) {
                    updateFormAddressInfo(zipCodeInfo);
                  }
                }}
                value={field.value}
              />
            )}
          </Field>

          <Field name="location.cityName">
            {(field: { error: any; value: any }, props) => (
              <TextInput
                {...props}
                label="City"
                wrapperClass="col-md-5"
                required="true"
                error={field.error}
                type="text"
                value={field.value}
              />
            )}
          </Field>

          <Field name="location.stateCode">
            {(field: { error: any; value: any }, props) => (
              <TextInput
                {...props}
                label="State"
                required="true"
                wrapperClass="col-md-5"
                error={field.error}
                type="text"
                onChange$={(evt: { target: { value: string | number } }) => {
                  zipcodes.lookup(evt.target.value);
                }}
                value={field.value}
              />
            )}
          </Field>
        </div>

        <h4 class="text-2xl dark:text-white mb-2">Hours and Drinks</h4>
        <FieldArray name="hours">
          {(fieldArray) => (
            <>
              {fieldArray.items.map((item: any, index: any) => {
                return (
                  <div key={item} class="flex flex-wrap -mx-3 mb-6">
                    <div class="w-full md:w-1/4 px-3 mb-6 md:mb-0">
                      <Field name={`${fieldArray.name}.${index}.day`}>
                        {(field: { error: any; value: any }, props) => (
                          <>
                            <Select
                              {...props}
                              value={field.value}
                              options={[
                                { label: "Sunday", value: "0" },
                                { label: "Monday", value: "1" },
                                { label: "Tuesday", value: "2" },
                                { label: "Wednesday", value: "3" },
                                { label: "Thursday", value: "4" },
                                { label: "Friday", value: "5" },
                                { label: "Saturday", value: "6" },
                              ]}
                              error={field.error}
                              label="Day of week"
                            />
                          </>
                        )}
                      </Field>
                    </div>

                    <div class="w-full md:w-1/4 px-3 mb-6 md:mb-0">
                      <Field name={`${fieldArray.name}.${index}.open`}>
                        {(field: { error: any; value: any }, props) => (
                          <TextInput
                            {...props}
                            label="Open "
                            wrapperClass="col-md-4"
                            required="true"
                            error={field.error}
                            type="time"
                            value={field.value}
                          />
                        )}
                      </Field>
                    </div>

                    <div class="w-full md:w-1/4 px-3 mb-6 md:mb-0">
                      <Field name={`${fieldArray.name}.${index}.close`}>
                        {(field: { error: any; value: any }, props) => (
                          <TextInput
                            {...props}
                            label="Close"
                            required="true"
                            error={field.error}
                            wrapperClass="col-md-4"
                            type="time"
                            value={field.value}
                          />
                        )}
                      </Field>
                    </div>
                    <div class="w-full md:w-1/4 px-3 mb-6 md:mb-0">
                      <Button
                        type="button"
                        onClick$={() =>
                          remove(barForm, "hours", {
                            at: index,
                          })
                        }
                      >
                        Delete
                      </Button>
                    </div>
                  </div>
                );
              })}

              <Button
                type="button"
                onClick$={() =>
                  insert(barForm, "hours", {
                    value: { day: "Monday", open: "", close: "" },
                  })
                }
              >
                Add row
              </Button>
            </>
          )}
        </FieldArray>

        {/* <div class="mb-3">
      <label for="drinks" class="form-label">
        Drinks
      </label>
      <input
        type="text"
        class="form-control"
        id="drinks"
        name="drinks"
        placeholder="Enter drinks separated by commas"
      />
    </div> */}

        <h4 class="text-2xl dark:text-white mb-2">TV Details</h4>

        <Field name="tvDetails.totalTvs" type="number">
          {(field: { error: any; value: any }, props) => (
            <TextInput
              {...props}
              label="Total TVs"
              error={field.error}
              type="number"
              value={field.value}
            />
          )}
        </Field>

        <Field name="tvDetails.indoorTvs" type="number">
          {(field: { error: any; value: any }, props) => (
            <TextInput
              {...props}
              label="Indoor TVs"
              error={field.error}
              type="number"
              value={field.value}
            />
          )}
        </Field>

        <Field name="tvDetails.outdoorTvs" type="number">
          {(field: { error: any; value: any }, props) => (
            <TextInput
              {...props}
              label="Outdoor TVs"
              error={field.error}
              type="number"
              value={field.value}
            />
          )}
        </Field>

        <Field name="tvDetails.averageTvSize" type="number">
          {(field: { error: any; value: any }, props) => (
            <TextInput
              {...props}
              label="Average TV Size"
              error={field.error}
              type="number"
              value={field.value}
            />
          )}
        </Field>

        <Field name="tvDetails.largestTvSize" type="number">
          {(field: { error: any; value: any }, props) => (
            <TextInput
              {...props}
              label="Largest TV Size"
              error={field.error}
              type="number"
              value={field.value}
            />
          )}
        </Field>

        <h4 class="text-2xl dark:text-white mb-2">Images</h4>
        <FieldArray name="images">
          {(fieldArray) => (
            <>
              {fieldArray.items.map((item, index) => (
                <div key={item} class="flex flex-wrap -mx-3 mb-6">
                  <Field name={`images.${index}.file`} type="File">
                    {(field, props) => (
                      <FileInput
                        {...props}
                        value={field.value}
                        error={field.error}
                        label="File list"
                      />
                    )}
                  </Field>
                  <div class="w-full md:w-1/4 px-3 mb-6 md:mb-0">
                    <Field name={`images.${index}.type`}>
                      {(field: { error: any; value: any }, props) => (
                        <>
                          <label
                            for={props.name}
                            class="block text-gray-700 text-sm font-bold mb-2"
                          >
                            Image type <span>*</span>
                          </label>
                          <select
                            {...props}
                            class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                            onSelect$={(it) => (field.value = it)}
                          >
                            <option value="logo">Logo</option>
                            <option value="interior">Interior</option>
                            <option value="exterior">Exterior</option>
                            <option value="fooddrink">Food/Drink</option>
                          </select>
                        </>
                      )}
                    </Field>
                  </div>

                  <div class="w-full md:w-1/4 px-3 mb-6 md:mb-0">
                    <Button
                      type="button"
                      onClick$={() =>
                        remove(barForm, "images", {
                          at: index,
                        })
                      }
                    >
                      Delete
                    </Button>
                  </div>
                </div>
              ))}

              <div class="w-full mb-6 md:mb-3">
                <Button
                  type="button"
                  onClick$={() =>
                    insert(barForm, "images", {
                      value: { type: "logo", file: undefined },
                    })
                  }
                >
                  Add image
                </Button>
              </div>
            </>
          )}
        </FieldArray>

        <Response class="pt-8 md:pt-10 lg:pt-12" of={barForm} />

        <Button type="submit">Submit</Button>
      </Form>
    </>
  );
});

System Info:


  System:
    OS: Windows 11 10.0.22635
    CPU: (16) x64 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
    Memory: 9.03 GB / 31.70 GB
  Binaries:
    Node: 18.17.1 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 10.2.3 - C:\Program Files\nodejs\npm.CMD
    pnpm: 8.6.7 - ~\AppData\Roaming\npm\pnpm.CMD
  npmPackages:
    @builder.io/partytown: ^0.8.0 => 0.8.1
    @builder.io/qwik: 1.2.10 => 1.2.10
    @builder.io/qwik-auth: 0.1.1 => 0.1.1
    @builder.io/qwik-city: ^1.2.10 => 1.2.17
    @modular-forms/qwik: ^0.21.0 => 0.21.0
    undici: ^5.23.0 => 5.27.2
    vite: ^4.4.7 => 4.5.0
cwoolum commented 1 year ago

Roling back to @builder.io/qwik@1.2.6 seems to fix the issue.

  npmPackages:
    @builder.io/partytown: ^0.8.0 => 0.8.1
    @builder.io/qwik: 1.2.6 => 1.2.6
    @builder.io/qwik-auth: 0.1.1 => 0.1.1
    @builder.io/qwik-city: 1.2.6 => 1.2.6
    undici: ^5.23.0 => 5.27.2
    vite: ^4.4.7 => 4.5.0
fabian-hiller commented 1 year ago

Thank you for creating this issue and for the additional information. This error should only occur when await event.request.formData() is called twice. Since Modular Forms only calls it once, I suspect that the bug has an external origin.

cwoolum commented 8 months ago

Just getting back to this. I think this might be related to using a multi-part form. It might be reading it multiple times for the images. Trying to narrow this down further.

fabian-hiller commented 8 months ago

Thank you for the update!

rafalfigura commented 8 months ago

I have the same issue, this error is thrown before myHandler is executed formAction$(myHandler). When you add onPost request handler to the router file, it is able to get the file with parseBody. I will try to investigate more.

fabian-hiller commented 8 months ago

Are you getting the error because formData() is called multiple times?

harrywebdev commented 8 months ago

I get this error also, and from what I can tell, there is a await request.formData() happening on this line https://github.com/BuilderIO/qwik/blob/9959b1875ac2b5f70de28e3f97baf86bda0a56c6/packages/qwik-city/middleware/request-handler/request-event.ts#L328C35-L328C44

before the @modular-forms' own formActionQrl calls that for the second time.

fabian-hiller commented 8 months ago

Maybe Qwik has changed their implementation. Feel free to provide me with a minimal reproduction. request.formData() wasn't called in Qwik when I implemented the library.

rafalfigura commented 7 months ago

Hello, I was able to fix the issue in #194. Could you check if that works for you? :)

fabian-hiller commented 7 months ago

I reviewed the PR