You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Astro+Svelte订阅表单提交API报400错误:邮箱无法传递

Astro + Svelte订阅表单400错误:前端邮箱无法传递到API路由

开发订阅表单时,提交返回400错误。API路由硬编码邮箱时功能正常,但前端邮箱无法传递到路由。具体情况如下:

配置概述

  • 客户端(Svelte组件):表单收集邮箱,通过fetch提交到/api/sendEmail.json,请求头设为Content-Type: application/json,请求体用JSON.stringify({ email: email })序列化。
  • 服务端(API路由):通过await request.json()解析请求体,但收到的请求体为空字符串。

已尝试的排查步骤

  • 验证表单提交触发正常,email变量更新符合预期;
  • 浏览器开发者工具确认请求方法为POST、Content-Type头正确、请求负载含预期JSON;
  • 在handleSubmit中硬编码email,排除变量本身问题;
  • 检查全局fetch拦截器,确保请求未被修改;
  • 测试其他API端点,排除端点特定问题;
  • 简化服务端逻辑,打印原始请求体和头。

代码片段

Svelte表单组件

<script lang="ts">
  import { onMount } from "svelte";
  import Reload from "svelte-radix/Reload.svelte";
  import { Button } from "@/components/ui/button/index";
  import { toast } from "svelte-sonner";

  let email = "";
  let loading = false;
  let formStatus = "";

  const handleSubmit = async (e) => {
    e.preventDefault();
    loading = true;
    formStatus = "";

    try {
      console.log(email);
      const response = await fetch("/api/sendEmail.json", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          email: email,
        }),
      });

      if (response.ok) {
        const data = await response.json();
        if (data.message === "Subscription successful") {
          formStatus = "success";
          toast("Subscription successful", {
            description: "You have been subscribed to the newsletter.",
          });
        } else {
          formStatus = "error";
          toast("Subscription failed", {
            description:
              "There was an error subscribing you to the newsletter.",
          });
        }
      } else {
        formStatus = "error";
        toast("Subscription failed", {
          description: "There was an error subscribing you to the newsletter.",
        });
      }
    } catch (err) {
      console.error(err);
      formStatus = "error";
      toast("Subscription failed", {
        description: "There was an error subscribing you to the newsletter.",
      });
    } finally {
      loading = false;
    }
  };

  // Log any errors during component initialization
  onMount(() => {
    window.onerror = function (msg, url, lineNo, columnNo, error) {
      console.error(
        "Error occurred:",
        msg,
        "URL:",
        url,
        "Line:",
        lineNo,
        "Column:",
        columnNo,
        "Error object:",
        error
      );
    };
  });
</script>

<form on:submit={handleSubmit}>
  <div class="overflow-hidden py-16 sm:py-24 lg:py-32">
    <div
      class="max-w-2xl gap-x-8 gap-y-16 lg:max-w-none flex place-content-center mx-auto"
    >
      <div class="max-w-xl lg:max-w-lg">
        <h2 class="text-3xl font-bold tracking-tight text-white sm:text-4xl">
          Subscribe to my Newsletter.
        </h2>
        <p class="mt-4 text-lg leading-8 text-gray-300">
          A Newsletter for Designers, Developers and Humans.
        </p>
        <div class="mt-6 flex max-w-md gap-x-4">
          <label for="email-address" class="sr-only"> Email address </label>
          <input
            name="email"
            type="email"
            autocomplete="email"
            required
            class="min-w-0 flex-auto rounded-md border-0 bg-white/5 px-3.5 py-2 text-white shadow-sm ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-neutral-500 sm:text-sm sm:leading-6"
            placeholder="Enter your email"
            bind:value={email}
          />
          <Button
            type="submit"
            class="flex-none rounded-md bg-neutral-200 px-3.5 py-2.5 text-sm font-semibold text-neutral-900 shadow-sm hover:bg-neutral-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:neutral-indigo-500"
          >
            {#if loading}
              <Reload class="mr-2 h-4 w-4 animate-spin" />
            {/if}
            Subscribe
          </Button>
        </div>
      </div>
    </div>
  </div>
</form>

Astro API路由

import type { APIRoute } from "astro";
import { Resend } from "resend";

const resend = new Resend(import.meta.env.RESEND_API_KEY);

export const POST: APIRoute = async ({ request }) => {
  try {
    const { email } = await request.json();
    console.log(email);
    const send = await resend.contacts.create({
      email,
      firstName: "",
      lastName: "",
      unsubscribed: false,
      audienceId: import.meta.env.RESEND_AUDIENCE_ID,
    });

    if (send.data) {
      return new Response(
        JSON.stringify({ message: "Subscription successful" }),
        {
          status: 200,
          statusText: "OK",
        }
      );
    } else {
      return new Response(JSON.stringify({ message: "Subscription failed" }), {
        status: 500,
        statusText: "Error",
      });
    }
  } catch (error) {
    console.error("Error parsing request body:", error);
    return new Response(JSON.stringify({ message: "Invalid request body" }), {
      status: 400,
      statusText: "Bad Request",
    });
  }
};

解决方案

1. 手动读取原始请求体解析

Astro的request.json()可能存在解析异常,尝试直接读取原始文本再手动解析,同时打印原始内容确认:

export const POST: APIRoute = async ({ request }) => {
  try {
    const rawBody = await request.text();
    console.log("Raw request body:", rawBody);
    const { email } = rawBody ? JSON.parse(rawBody) : {};
    
    if (!email) {
      return new Response(JSON.stringify({ message: "Email is required" }), {
        status: 400,
      });
    }

    const send = await resend.contacts.create({
      email,
      firstName: "",
      lastName: "",
      unsubscribed: false,
      audienceId: import.meta.env.RESEND_AUDIENCE_ID,
    });

    if (send.data) {
      return new Response(
        JSON.stringify({ message: "Subscription successful" }),
        { status: 200 }
      );
    } else {
      return new Response(JSON.stringify({ message: "Subscription failed" }), {
        status: 500,
      });
    }
  } catch (error) {
    console.error("Error processing request:", error);
    return new Response(JSON.stringify({ message: "Invalid request body" }), {
      status: 400,
    });
  }
};

2. 明确指定请求头字符编码

浏览器可能自动添加编码信息导致解析问题,修改fetch请求头:

headers: {
  "Content-Type": "application/json; charset=utf-8",
},

3. 检查Astro中间件是否消耗请求体

如果项目使用了中间件,确保中间件没有提前调用request.text()或request.json(),否则后续API路由无法再次读取请求体。

4. 改用FormData提交(备选方案)

若JSON方式始终异常,切换为FormData提交:

客户端修改:

const formData = new FormData();
formData.append("email", email);

const response = await fetch("/api/sendEmail.json", {
  method: "POST",
  body: formData,
});

服务端修改:

const formData = await request.formData();
const email = formData.get("email") as string;

5. 验证环境变量配置

确认import.meta.env.RESEND_AUDIENCE_ID和RESEND_API_KEY加载正常,避免因环境变量问题引发间接错误。


内容的提问来源于stack exchange,提问作者Pruthivi Thejan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 15:34:51