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
相关产品推荐
相关产品推荐

