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

Next.js 14生产环境PUT请求报405错误,无法更新theme.json

解决PUT请求405(Method Not Allowed)错误问题

我在网站生产模式下,尝试通过route.ts中的PUT方法更新public/database/theme.json的数据,但收到了**PUT Error Code 405 (Method Not Allowed)**错误。以下是相关代码文件:

public/database/theme.json

[{"theme":"system"}]

src/app/api/theme/route.ts

import { promises as fs } from "fs";

export const GET = async () => {
  try {
    const data = await fs.readFile("./public/database/theme.json", "utf-8");
    return new Response(data);
  } catch (error) {
    return new Response(JSON.stringify({ message: "Failed to get theme" }), { status: 500 });
  }
};

export const PUT = async (request: Request) => {
  try {
    const theme = await request.json();
    const themeStringify = JSON.stringify([theme]);
    await fs.writeFile("./public/database/theme.json", themeStringify, "utf-8");
    return new Response(JSON.stringify({ message: "Theme changed successfully" }), { status: 200 });
  } catch (error) {
    return new Response(JSON.stringify({ message: "Failed to change theme" }), { status: 500 });
  }
};

.env

NEXT_PUBLIC_API_URL="http://localhost:3000/"

src/utils/api/theme/index.tsx

const API_URL = process.env.NEXT_PUBLIC_API_URL + "/api/theme";

if (!API_URL) {
  throw new Error("The API URL is not defined. Please check your environment variables.");
}

export interface ITheme {
  theme: string;
}

export const GETTheme = async (): Promise<ITheme[]> => {
  try {
    const res = await fetch(API_URL, { cache: "no-cache" });

    if (!res.ok) {
      throw new Error(`Failed to fetch: Theme with status ${res.status}`);
    }

    return await res.json();
  } catch (error) {
    console.log(error);
    throw error;
  }
};

export const PUTTheme = async (data: ITheme): Promise<ITheme> => {
  try {
    const res = await fetch(API_URL, {
      body: JSON.stringify(data),
      headers: {
        "Content-Type": "application/json",
      },
      method: "PUT",
    });

    if (!res.ok) {
      throw new Error(`Failed to put: Theme with status ${res.status}`);
    }

    return await res.json();
  } catch (error) {
    console.log(error);
    throw error;
  }
};

src/layouts/home/modules/header/index.tsx

import { FC, ReactElement } from "react";

import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";

import { GETTheme } from "@/utils";

import { Navbar } from "./batches";

export const Header: FC = async (): Promise<ReactElement> => {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery({
    queryFn: GETTheme,
    queryKey: ["GETTheme"],
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <header>
        <Navbar />
      </header>
    </HydrationBoundary>
  );
};

src/layouts/home/modules/header/batches/navbar/index.tsx

"use client";

import { FC, ReactElement } from "react";

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTheme } from "next-themes";
import Image from "next/image";
import Link from "next/link";
import { BsList } from "react-icons/bs";

import { useGlobalStates } from "@/hooks/global";
import { ButtonTWM } from "@/interfaces/buttons/button";
import logoDIZETO from "@/public/assets/images/logos/dizeto.svg";
import { GETTheme, PUTTheme } from "@/utils";

import { IconBasedOnTheme } from "./IconBasedOnTheme";

export const LINKS_DATA = [
  { href: "#about", label: "About" },
  { href: "#portfolio", label: "Portfolio" },
  { href: "#pricing", label: "Pricing" },
  { href: "#testimony", label: "Testimony" },
  { href: "#client", label: "Client" },
  { href: "#contact", label: "Contact" },
];

export const Navbar: FC = (): ReactElement => {
  const { data: dataTheme } = useQuery({
    queryFn: GETTheme,
    queryKey: ["GETTheme"],
  });

  const queryClient = useQueryClient();
  const { setTheme, theme } = useTheme();
  const { openASide, setOpenASide } = useGlobalStates();

  const handleUpdateTheme = useMutation({
    mutationFn: PUTTheme,
    onSuccess: async () => {
      await queryClient.invalidateQueries({ queryKey: ["GETTheme"] });
    },
  });

  const handleTheme = () => {
    if (theme === "dark") {
      setTheme("system");
      handleUpdateTheme.mutate({ theme: "system" });
    } else if (theme === "system") {
      setTheme("light");
      handleUpdateTheme.mutate({ theme: "light" });
    } else {
      setTheme("dark");
      handleUpdateTheme.mutate({ theme: "dark" });
    }
  };

  return (
    <nav className="fixed left-0 top-0 w-full">
      <ul className="flex items-center justify-between px-10 py-3 font-semibold">
        <li className="flex size-[40px] items-center">
          <Link href="#hero">
            <Image alt="DIZETO" priority src={logoDIZETO} width={35} />
          </Link>
        </li>
        <li>
          <ul className="hidden sm:flex sm:items-center sm:gap-5 md:gap-10 lg:gap-16">
            {LINKS_DATA.map((dt, index) => (
              <li key={index}>
                <Link className={ButtonTWM({ color: "black", size: "sm", variant: "ghost" })} href={dt.href}>
                  {dt.label}
                </Link>
              </li>
            ))}
          </ul>
        </li>
        <li>
          <ul className="flex gap-2">
            <li className="flex size-[40px] items-center justify-end">
              <button className={ButtonTWM({ color: "black", size: "sm", variant: "ghost" })} onClick={handleTheme} type="button">
                <IconBasedOnTheme dataTheme={dataTheme} />
              </button>
            </li>
            <li className="flex size-[40px] items-center justify-end sm:hidden">
              <button className={ButtonTWM({ color: "black", size: "sm", variant: "ghost" })} onClick={() => setOpenASide(!openASide)} type="button">
                <BsList className="mr-[-4px]" size={30} />
              </button>
            </li>
          </ul>
        </li>
      </ul>
    </nav>
  );
};

src/layouts/home/modules/header/batches/navbar/IconBasedOnTheme.tsx

import { FC, ReactElement, useEffect, useState } from "react";

import { useTheme } from "next-themes";
import { BsFillMoonFill, BsFillSunFill } from "react-icons/bs";
import { SiMoonrepo } from "react-icons/si";

import { ITheme } from "@/utils";

type T = {
  dataTheme: ITheme[] | undefined;
};

export const IconBasedOnTheme: FC<T> = ({ dataTheme }): ReactElement => {
  const { theme } = useTheme();
  const [icon, setIcon] = useState<null | ReactElement>(null);

  useEffect(() => {
    switch (theme) {
      case "system":
        setIcon(<SiMoonrepo size={22} />);
        break;
      case "light":
        setIcon(<BsFillSunFill size={22} />);
        break;
      case "dark":
        setIcon(<BsFillMoonFill size={22} />);
        break;
    }
  }, [theme]);

  const initialIconValue = () => {
    if (dataTheme?.[0].theme === "system") {
      return <SiMoonrepo size={22} />;
    } else if (dataTheme?.[0].theme === "light") {
      return <BsFillSunFill size={22} />;
    } else {
      return <BsFillMoonFill size={22} />;
    }
  };

  return icon ?? initialIconValue();
};

问题原因及解决方案

1. 生产环境文件系统只读

Vercel、Netlify等主流托管平台的生产环境中,public目录属于静态资源目录,是只读文件系统,无法通过fs.writeFile直接修改文件内容,平台会拦截此类修改请求,导致405错误。

解决方案:

  • 改用数据库(如Supabase、PlanetScale)存储主题设置,后端API通过数据库操作完成读写。
  • 若仅需用户个性化主题,直接将设置存在客户端localStorage中,配合next-themes使用,无需后端存储。

2. 环境变量配置错误

生产环境中NEXT_PUBLIC_API_URL可能未设置为正确的生产域名,导致请求发送到错误地址,间接引发405错误。

解决方案:

  • 在生产环境的环境变量中,将NEXT_PUBLIC_API_URL设置为实际生产域名(如https://your-domain.com/),确保API请求指向正确地址。

3. 简化客户端主题处理(替代方案)

如果主题设置仅用于用户个人偏好,无需全局同步,可直接在客户端处理:

// 修改Navbar中的handleTheme函数
const handleTheme = () => {
  let newTheme: string;
  if (theme === "dark") {
    newTheme = "system";
  } else if (theme === "system") {
    newTheme = "light";
  } else {
    newTheme = "dark";
  }
  setTheme(newTheme);
  localStorage.setItem("theme", newTheme);
};

在根布局或_app.tsx中读取初始主题:

import { useEffect } from 'react';
import { useTheme } from 'next-themes';

export default function RootLayout({ children }) {
  const { setTheme } = useTheme();

  useEffect(() => {
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) setTheme(savedTheme);
  }, [setTheme]);

  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

内容的提问来源于stack exchange,提问作者Gede Dewo Wahyu M.W

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 06:32:33