QwikDev / qwik

Instant-loading web apps, without effort
https://qwik.dev
MIT License
20.79k stars 1.3k forks source link

[🐞]Error using server$() on Vercel #4857

Closed Maikpwwq closed 1 year ago

Maikpwwq commented 1 year ago

Which component is affected?

Qwik City (routing)

Describe the bug

I have a Qwik app that uses Static adapter. In one of my components I made a request into a server$(async function(data){ const resume = await connectionDB(data); ...}), it all runs correctly in my local enviroment, catchin my service mongo-init.js. Then I deploy my project in Vercel. During build stage everything works fine. But when in production I try to request, I get this Error:

XHRPOST
https://www.nexasoft.dev/?qfunc=Jouz36DzMf4
[HTTP/2 404 Not Found 951ms]

Why it is calling that route in my project. ie /?qfunc=....

Reproduction

https://github.com/Maikpwwq/nexasoft.git

Steps to reproduce

my mongo-init.js file:

/* eslint-disable @typescript-eslint/no-var-requires */
import mongoose from "mongoose"; // ,

import * as dotenv from "dotenv";
dotenv.config();

// Variables de entorno
const DB_USER = `${process.env.VITE_DB_USER}`;
const DB_PASSWORD2 = `${process.env.VITE_DB_PASSWORD2}`;
const DB_NAME = `${process.env.VITE_DB_NAME}`;
const MONGODB_COLLECTION = `${process.env.VITE_MONGODB_COLLECTION}`;
// const MONGO_HOST = `mongodb+srv://${DB_USER}:${DB_PASSWORD2}@${DB_HOST}/?retryWrites=true&w=majority`;
const MONGO_HOST = `${process.env.VITE_MONGO_HOST}`;

const schema = new mongoose.Schema({
  name: String,
  email: String,
  phone: String,
  issue: String,
  message: String,
});

const options = {
  dbName: DB_NAME,
  user: DB_USER,
  pass: DB_PASSWORD2,
};

export async function connectionDB(contactData) {
  const dbPromise = mongoose.createConnection(MONGO_HOST, options);
  const userModel = dbPromise.model(MONGODB_COLLECTION, schema);
  dbPromise.once("open", function () {
    console.log("Connected successfully to your DB");
  });
  const res = await userModel.create(contactData);
  console.log("create userModel", res);
  return res;
}

my FormComponent.jsx file:

import { component$, useSignal } from "@builder.io/qwik";
import { connectionDB } from "~/services/mongo-init";
import {
  server$,
  useNavigate,
} from "@builder.io/qwik-city";
export const addCustomer = server$(async function (data) {
  // This will only run on the server when the user submits the form (or when the action is called programatically)
  const customerRecord = {
    name: data.name.value,
    email: data.email.value,
    phone: data.phone.value,
    issue: data.issue.value,
    message: data.message.value,
  };
  const resume = await connectionDB(customerRecord);
  console.log("Promise message", resume);

  return {
    successful: true,
  };
});
export default component$(() => {
  const nav = useNavigate();

  const formData = {
    name: useSignal(""),
    email: useSignal(""),
    phone: useSignal(""),
    issue: useSignal(""),
    message: useSignal(""),
  };

  return (
    <div class={styles.sheetFormStyle}>
      <h6>
        Formulario de contacto
      </h6>
      <p className="pt-2 pb-4" align="center">
        Solicita información adicional o una presentación de nuestros servicios.
      </p>
      <div
        class={styles.formFlex}
      >
        <label for="form-name" class={styles.labelStyle}>
          Nombre:{" "}
        </label>
        <input
          id="form-name"
          name="fullName"
          type="text"
          class={styles.inputStyle}
          bind:value={formData.name}
        />
        <label for="form-email" class={styles.labelStyle}>
          Correo electrónico:{" "}
        </label>
        <input
          id="form-email"
          name="email"
          type="email"
          class={styles.inputStyle}
          bind:value={formData.email}
        />
        <label for="form-phone" class={styles.labelStyle}>
          Teléfono:{" "}
        </label>
        <input
          id="form-phone"
          name="phone"
          type="phone"
          class={styles.inputStyle}
          bind:value={formData.phone}
        />
        <label for="form-issue" class={styles.labelStyle}>
          Asunto:{" "}
        </label>
        <input
          id="form-issue"
          name="issue"
          type="text"
          class={styles.inputStyle}
          bind:value={formData.issue}
        />
        <label for="form-message" class={styles.labelStyle}>
          Mensaje:{" "}
        </label>
        <textarea
          id="form-message"
          rows={7}
          name="message"
          class={styles.inputStyle}
          bind:value={formData.message}
        />
        <button
          // type="submit"
          class={styles.btnStyle}
          onClick$={async () => {
            const resume = await addCustomer(formData);
            console.log("greeting", resume);
            const resetForm = () => {
              formData.name.value = "";
              formData.email.value = "";
              formData.phone.value = "";
              formData.issue.value = "";
              formData.message.value = "";
            };
            if (resume) {
              alert("Gracias, su mensaje ha sido recibido.");
              resetForm();
              await nav("/");
            }
          }}
        >
          Enviar
        </button>
      </div>
    </div>
  );
});

System Info

System:
    OS: Linux 5.19 Ubuntu 22.04.2 LTS 22.04.2 LTS (Jammy Jellyfish)
    CPU: (16) x64 AMD Ryzen 7 5700G with Radeon Graphics
    Memory: 5.49 GB / 15.01 GB
    Container: Yes
    Shell: 5.1.16 - /bin/bash
  Binaries:
    Node: 20.1.0 - ~/.nvm/versions/node/v20.1.0/bin/node
    Yarn: 1.22.19 - /usr/bin/yarn
    npm: 9.6.4 - ~/.nvm/versions/node/v20.1.0/bin/npm
    pnpm: 8.6.8 - ~/.local/share/pnpm/pnpm
  npmPackages:
    @builder.io/partytown: ^0.8.0 => 0.8.0 
    @builder.io/qwik: ^1.2.6 => 1.2.6 
    @builder.io/qwik-city: ^1.2.6 => 1.2.6 
    @builder.io/qwik-react: 0.5.0 => 0.5.0 
    undici: 5.22.1 => 5.22.1 
    vite: 4.4.7 => 4.4.7

Additional Information

No response

Maikpwwq commented 1 year ago

This is a minimal reproduction, abstracted from my main project. I need to solve this issue now. How to use forms in Qwik? The documentation talks about @modular-forms/qwik I follow the configuration but it still fails. Really I have prove almost everything, serverless functions on Vercel, server$ functions on Qwik, now actions$ and I am a bit frustrated with this. It works in development mode in production deploy continue failing.

Captura desde 2023-08-25 02-06-19

I have tried first with mongo, now with supabase the problem persist. My file Index.tsx

import { component$, $, useTask$ } from "@builder.io/qwik"; // , useSignal
import { isServer } from "@builder.io/qwik/build";
import { createClient } from "@supabase/supabase-js";
import { v4 as uuidv4 } from 'uuid'
import clsx from "clsx";
import {
  routeLoader$,
  z,
} from "@builder.io/qwik-city";
import type { InitialValues, SubmitHandler } from "@modular-forms/qwik"; //
import {
  useForm,
  formAction$,
  zodForm$,
  reset,
} from "@modular-forms/qwik";
import styles from "~/components/modular-forms/modularForm.module.css";
import { MUITypography, MUIPaper } from "~/integrations/react/mui";
import { TextInput } from "~/components/modular-forms/TextInput";

const SUPABASE_URL = `${import.meta.env.VITE_SUPABASE_URL}`;
const SUPABASE_KEY = `${import.meta.env.VITE_SUPABASE_KEY}`;

type LoginForm = {
  name: string;
  email: string;
  phone: string;
  issue: string;
  message: string;
};

const loginSchema = z.object({
  name: z.string().min(1, "Por favor introduzca su nombre."),
  email: z
    .string()
    .min(1, "Por favor introduzca su email.")
    .email("The email address is badly formatted."),
  phone: z
    .string()
    .min(1, "Por favor introduzca su teléfono.")
    .min(10, "Tu teléfono debe tener 10 caracteres o más."),
  issue: z.string().min(1, "Por favor introduzca su asunto."),
  message: z
    .string()
    .min(1, "Por favor introduzca su mensaje.")
    .min(8, "Tu mensaje debe tener 8 caracteres o más."),
});

// Also posible infer typos
// type LoginForm = z.infer<typeof loginSchema>;

// can only be declared in `layout.tsx`, `index.tsx` and `plugin.tsx` inside the src/routes directory
export const useFormLoader = routeLoader$<InitialValues<LoginForm>>(() => ({
  name: "",
  email: "",
  phone: "",
  issue: "",
  message: "",
}));

type ResponseData = {
  customerId: string;
};

export const useFormAction = formAction$<LoginForm, ResponseData>(
  async (values) => {
    // Runs on SERVER
    console.log("useFormAction", values);
    try {
      // Create a single supabase client for interacting with your database
      const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
      const { name, email, phone, issue, message } = values;
      const recordID : string = uuidv4();
      const hexNumber : number = 1; // parseInt(recordID.replace(/-/g, ''), 16);
      const { data: customer_form, error } = await supabase
        .from("customer_form")
        .insert([{ id: hexNumber, created_at: new Date(), name, email, phone, issue, message }])
        .select("*");

      console.log("supabase contact form", customer_form, error);
      if (customer_form) {
        console.log("Success supabase contact form", customer_form[0].id);
      }

      if (error) {
        console.log("Error supabase contact form", error);
      }

      return {
        status: "success",
        message: `Gracias, su mensaje ha sido recibido. ${recordID}`,
        data: { customerId: recordID },
      };
    } catch (error) {
      console.error(error);
      return {
        status: "error",
        message: `No se ha podido enviar su mensaje. ${error}`,
        data: { customerId: "" },
      };
    }
  },
  zodForm$(loginSchema),
); // valiForm$(LoginSchema)

export default component$(() => {
  // const nav = useNavigate();

  // , FieldArray
  const [loginForm, { Form, Field }] = useForm<LoginForm, ResponseData>({
    loader: useFormLoader(),
    action: useFormAction(),
    validate: zodForm$(loginSchema),
  });

  const handleSubmit = $<SubmitHandler<LoginForm>>(
    async (values: any, event: any) => {
      // Runs on CLIENT
      console.log("handleSubmit", values, event);
    },
  );

  const successData = $(async () => {
    console.log(
      "handleSubmitSuccess",
      loginForm.submitted,
      loginForm.submitting,
      loginForm.response,
    );
    alert(loginForm.response.message);
    reset(loginForm); // , useFormLoader
    // clearResponse(loginForm);
    // const value = getValue(form, name, options);
    // await nav("/");
  });

  const errorData = $(async () => {
    console.log(
      "handleSubmitError",
      loginForm.submitted,
      loginForm.submitting,
      loginForm.response,
    );
    alert(loginForm.response.message);
  });

  useTask$(({ track }) => {
    track(() => loginForm.response.status);
    if (isServer) {
      return; // Server guard
    }
    if (
      loginForm.submitted &&
      loginForm.submitting === false &&
      loginForm.response.status === "success"
    ) {
      successData();
    } else if (
      loginForm.submitted &&
      loginForm.submitting === false &&
      loginForm.response.status === "error"
    ) {
      errorData();
    }
  });

  return (
    <div class="container container-center flex justify-center" style={{}}>
      <MUIPaper className={styles.cardContactForm} elevation={16}>
        <div class={styles.sheetFormStyle}>
          <MUITypography
            variant="h6"
            color={"var(--qwik-dark-blue)"}
            align="center"
          >
            Formulario de contacto
          </MUITypography>
          <MUITypography variant="body1" className="pt-2 pb-4" align="center">
            Solicita información adicional o una presentación de nuestros
            servicios.
          </MUITypography>
          <Form
            class={styles.formFlex}
            onSubmit$={handleSubmit}
            // preventdefault:submit
            // reloadDocument={true}
          >
            <Field
              name="name"
            >
              {(field, props) => (
                <TextInput
                  {...props}
                  value={field.value}
                  error={field.error}
                  type="text"
                  label="Nombre:"
                  placeholder="Nombre"
                  required
                />
              )}
            </Field>
            <Field
              name="email"
            >
              {(field, props) => (
                <TextInput
                  {...props}
                  value={field.value}
                  error={field.error}
                  type="email"
                  label="Email:"
                  placeholder="Correo electrónico"
                  required
                />
              )}
            </Field>
            <Field
              name="phone"
            >
              {(field, props) => (
                <TextInput
                  {...props}
                  value={field.value}
                  error={field.error}
                  type="tel"
                  label="Teléfono:"
                  placeholder="+57"
                  required
                />
              )}
            </Field>
            <Field
              name="issue"
            >
              {(field, props) => (
                <TextInput
                  {...props}
                  value={field.value}
                  error={field.error}
                  type="text"
                  label="Asunto:"
                  placeholder="Asunto"
                  required
                />
              )}
            </Field>
            <Field
              name="message"
            >
              {(field, props) => (
                <TextInput
                  {...props}
                  value={field.value}
                  error={field.error}
                  type="text"
                  label="Mensaje:"
                  placeholder="Mensaje"
                  required
                />
              )}
            </Field>
            <button
              type="submit"
              class={clsx("mx-3 lg:mx-5", styles.btnStyle)}
            >
              Enviar
            </button>
          </Form>
        </div>
      </MUIPaper>
    </div>
  );
});
mhevery commented 1 year ago

Are you by any chance generating SSG (static site?) If so then server$ can't work as there is no server.

Does npm run preview work? That should be same as replayed behavior

Maikpwwq commented 1 year ago

@mhevery Yes this site use Adapters for SSG. This because Vercel deploy only static content. Use server$() was one of my first alternatives, after I tried with express adapter, and get that response about Vercel site content. So I tried again with routeLoader$() and routeAction$() doing a page instead a component.

When I execute command npm run preview it works on my local.

I have store forms sucessfully from NuxtJS and ViteJS in Vercel projects.

mhevery commented 1 year ago

None of the static server solutions can work with serevr$ or routeAction$() solution as all of this requires as server running JS. So I don't think this is a Qwik bug.

I am going to close this issue as this is a limitation of SSG.

Maikpwwq commented 1 year ago

@mhevery why it is not about Qwik if Nuxt and Vite-plugin-ssr bouth are SSR frameworks by default and they work in Vercel?