I am currently creating a web homepage utilizing svelteKit.

I am creating a system where when a user enters the /typo path, they fill in the form on the page and when they press the Submit button, it creates and sends an email via Googlemail. However, in my local environment (development server), the emails are coming through fine, but after deployment, I am getting a 400 Bad Request error. Can you tell me what the problem is based on my code and screenshots?

I have set the deployment environment to adapter-node based on node.js and after build, I run index.js in build folder using pm2.

Why do I have this problem after deploying a feature that worked fine in my development environment (localhost)?

the written code


//svelte.config.js

import adapter from '@sveltejs/adapter-node';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    adapter: adapter({
      host: '0.0.0.0',
      port: 5173,
    }),
  }
};

export default config;
// src/hooks.js

export async function handle({ request, resolve }) {
  const response = await resolve(request);
  response.headers.set('Access-Control-Allow-Origin', '*');
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  response.headers.set('Access-Control-Allow-Headers', '*');
  response.headers.set('Access-Control-Allow-Credentials', 'true');

  return response;
}
// src/lib/server/sendEmail.js
import nodemailer from "nodemailer";
import { GMAIL_ADDRESS, GMAIL_PASS } from "$env/static/private";

const transporter = nodemailer.createTransport({
  service: "gmail",
  auth: {
    user: GMAIL_ADDRESS,
    pass: GMAIL_PASS,
  },
  tls: {
    rejectUnauthorized: false,
  },
});

const sendEmail = async (subject, content) => {
  try {
    const info = await transporter.sendMail({
      from: GMAIL_ADDRESS,
      to: GMAIL_ADDRESS, 
      subject: subject,
      html: content, 
    });
    console.log("Email sent: ", info.messageId);
  } catch (error) {
    console.error("sendEmail error: ", error);
    throw error;
  }
};

export default sendEmail;
// src/routes/typo/+server.js

import upload from '$lib/server/upload.js';
import { json, error } from '@sveltejs/kit';
import sendEmail from '$lib/server/sendEmail.js';
import crypto from 'crypto';
import { CAPTCHA_SALT } from '$env/static/private';

export async function POST({ request, cookies }) {
  const data = await request.formData();
  const captchaText = data.get('captchaText');
  const hash = data.get('hash');

  let userHash = crypto.createHash('md5').update(captchaText + CAPTCHA_SALT).digest('hex');
  if (hash !== userHash) {
    throw error(400, 'Captcha verification failed');
  }

  const name = data.get('name');
  const bookTitle = data.get('bookTitle');
  const email = data.get('email');
  const phone = data.get('phone');
  const inquiry = data.get('inquiry');

  const content =
`<p>이름: ${name}</p>
<p>이메일: ${email}</p>
<p>연락처: ${phone}</p>
<p>책 제목: ${bookTitle}</p>
<p style="white-space: pre-line;">오탈자 내용: ${inquiry}</p>
`

  try {
    await sendEmail(
      `[오타 제보] ${name} 님 | ${bookTitle}`, // 메일 제목
      content, 
      email 
    );
  } catch (err) {
    console.error(err);
    throw error(500, 'Failed to send email');
  }

  return json({ message: 'ok' });
}

<!--src/routes/typo/+page.svelte-->

<script>
  export let data;

  import BookSearch from "../../components/BookSearch.svelte";
    import ContentHeading from "../../components/ContentHeading.svelte";
    import SubmitButton from "../../components/SubmitButton.svelte";
    import TitleSection from "../../components/TitleSection.svelte";

  let captchaText = '';

  let books = data.books;

  let bookId = null;
  let selectedBook = null;
  let name = '';
  let phone = '';
  let email = '';
  let inquiry = '';

  const onSelectBook = (id) => {
    bookId = id;
    selectedBook = books.find(book => book.id === id);
  }

  const submit = async () => {
    if (!confirm('제출하시겠습니까?')) return;

    if (!name) { alert('이름을 입력하세요.'); return; }
    if (!email) { alert('이메일을 입력하세요.'); return; }
    let emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    if (!emailRegex.test(email)) { alert('이메일 형식이 올바르지 않습니다.'); return; }
    if (!phone) { alert('연락처를 입력하세요.'); return; }
    if (!bookId) { alert('도서를 선택하세요.'); return; }
    if (!captchaText) { alert('자동입력방지 문자를 입력하세요.'); return; }

    const formData = new FormData();

    formData.append('name', name);
    formData.append('email', email);
    formData.append('phone', phone);
    formData.append('bookTitle', selectedBook.attributes.title);
    formData.append('inquiry', inquiry);
    formData.append('captchaText', captchaText);
    formData.append('hash', data.hash);

    const res = await fetch('/typo', {
      method: 'POST',
      body: formData,
    });

    if (res.status === 200) {
      alert('제출되었습니다.');
    } else {
      let errorText = (await res.json()).message;
      alert(`제출에 실패했습니다.\n${errorText}`);
    }
  }

</script>

<TitleSection title="오탈자 제보" />

<div class="about-mission-area pb-70">
  <div class="container">
    <ContentHeading name="오탈자 제보" />
    <div class="row">
      <div class="col-lg-12 col-md-12">
        <div class="container"  style="padding:0px">
          <form>
            <table class="table" >
              <tbody>
                <tr class="table-row">
                  <td class="bg-gray-3 header-text">이름</td>
                  <td><input bind:value={name} type="text" class="form-control" id="name" placeholder="이름을 입력하세요"></td>
                </tr>
                <tr class="table-row">
                  <td class="bg-gray-3 header-text">이메일</td>
                  <td><input bind:value={email} type="email" class="form-control" id="email" placeholder="이메일 주소를 입력하세요"></td>
                </tr>
                <tr class="table-row">
                  <td class="bg-gray-3 header-text">연락처</td>
                  <td><input bind:value={phone} type="tel" class="form-control" id="phone" placeholder="연락처를 입력하세요"></td>
                </tr>
                <tr class="table-row">
                  <td class="bg-gray-3 header-text">도서명</td>
                  <td class="book-search">
                    <div class="book-title">
                      <span>{selectedBook ? selectedBook.attributes.title : "책을 선택해주세요."}</span>
                    </div>
                    <BookSearch books={books} onSelect={onSelectBook}/>
                  </td>
                </tr>
                <tr class="table-row">
                  <td class="bg-gray-3 header-text">오탈자 내용</td>
                  <td><textarea bind:value={inquiry} class="form-control" id="exampleFormControlTextarea1" rows="8"></textarea></td>
                </tr>
              </tbody>
            </table>
            <!--captcha-->
            <div class="d-flex flex-column align-items-center mb-3">
              {@html data.captcha}
              <input bind:value={captchaText} type="text" class="form-control mt-2" id="captcha" placeholder="위의 문자를 입력하세요">
            </div>
            <SubmitButton onClick={submit} text="제출"/>
          </form>
        </div>
      </div>
    </div>
  </div>
</div>

<style>
  .book-search {
    display: flex;
    flex-direction: column;
  }


  #captcha {
    max-width: 260px;
  }

  .table-row {
    padding-bottom: 10px !important;
    border-bottom: 5px solid white !important;
    font-family: 'Nanum Gothic', sans-serif !important;
    font-weight: 700 !important;
  }

  .header-text {
    text-align: left;
    vertical-align: middle !important;
    font-size: 1rem;
    width: 150px;
  }
</style>

Any help would be greatly appreciated.

  1. checked if it's a port forwarding issue for using smtp on the deployed server, but everything is fine.

  2. Checked if it's a problem with environment variables, but it works fine in the development environment.

  3. I tried specifying the server domain directly in the request URL in '/typo'. But it didn't work.

1

There are 1 best solutions below

0
On

I tried to add as many details as possible in the comment, but it seems to be better off writing it here. First, I don't think this is the issue from SvelteKit. More like a PM2 config issue.

To check if it's not, try run this command:

PORT=YOURPORT ORIGIN=http://website.com node index.js 

If it doesn't raise any CORS error, got ya! If not, it may be indeed from Sveltekit.

First stop pm2, and disable your app.

pm2 stop index.js && pm2 disable [processID]

Add a config file in your app folder.

{
    "apps" : [
        {
            "name": "from",
            "script": "./production/index.js", <- your index.js file location
            "node_args": [
            ],
            "autorestart": false,
            "exec_interpreter": "node",
            "exec_mode": "cluster",
            "instances": 1,
            "env": {
                "NODE_ENV": "development"
            },
            "env_production": {
                "ORIGIN": "https://yourwebsite.com", <- ORIGIN
                "PORT": "PORT" <- Remember ""
            }
        }
    ]
}

(This is the file I use on my production server. You may need to tweak some settings)

Start PM2

pm2 start .server_config.json --env production 

Remember --env production. Check if it works or not. Also, don't add any catch-all * to Access-Control-Allow-Origin. Anyone can abuse your form and Gmail API.