I’m experiencing an issue with an API route in a Next.js application deployed on Vercel. The API route should accept POST
requests, but intermittently, I’m getting a 405 “Method Not Allowed” error as if the request was converted to a GET
.
Problem Description
- The application works correctly on
localhost
. - It happens only in my search engine dialog. My other api routes work fine.
- In production, some requests to the API work fine, while others fail.
- In the browser’s network tab, I can see the request is sent as
POST
, but the server responds with a 405 and logs the request asGET
.
But when clicking in the failed request:
This print shows up: {"error":"Method Not Allowed","msg":"GET","url":"/api/search/get-initial-results"}
Here is my api route:
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
res
.status(405)
.json({
error: "Method Not Allowed",
msg: req.method,
url: req.url,
});
return;
}
const {
currentTab,
}: {
currentTab: "all" | "posts" | "writers" | "tags";
} = req.body;
const identifier = "api";
const result = await ratelimit.limit(identifier);
res.setHeader("X-RateLimit-Limit", result.limit);
res.setHeader("X-RateLimit-Remaining", result.remaining);
if (!result.success) {
res.status(429).json({
message: "The request has been rate limited.",
rateLimitState: result,
});
return;
}
let results = {};
switch (currentTab) {
case "all":
// Search in users
const { data: usersData, error: usersError } = await supabase
.from("user")
.select("id, name, username")
.eq("boosted", true)
.limit(8);
if (usersError)
return res.status(500).json({ error: usersError.message });
// Search in posts
const { data: postsData, error: postsError } = await supabase
.from("post")
.select("id, title, slug")
.eq("boosted", true)
.limit(8)
.eq("published", true);
if (postsError)
return res.status(500).json({ error: postsError.message });
results = {
users: usersData,
posts: postsData,
tags: tags,
};
break;
case "posts":
const { data: postsDataPosts, error: postsErrorPosts } = await supabase
.from("post")
.select("id, title, slug")
.eq("published", true)
.limit(8);
if (postsErrorPosts)
return res.status(500).json({ error: postsErrorPosts.message });
results = {
posts: postsDataPosts,
};
break;
case "writers":
const { data: usersDataWriters, error: usersErrorWriters } =
await supabase
.from("user")
.select("id, name, username")
.eq("boosted", true)
.limit(8);
if (usersErrorWriters)
return res.status(500).json({ error: usersErrorWriters.message });
results = {
users: usersDataWriters,
};
break;
case "tags":
results = {
tags: tags,
};
break;
default:
break;
}
res.status(200).json(results);
res.status(200).json(results);
}
And here is how I call it:
const getInitialResults = React.useCallback(async () => {
const response = await fetch(`/api/search/get-initial-results`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
currentTab,
}),
});
const res = await response.json();
if (res.error) {
setResults(null);
setStatus("error");
return;
}
setResults(res);
setStatus("idle");
}, [currentTab]);
const updates = useDebouncedCallback(
(normalizedText: string) => {
setStatus("loading");
if (normalizedText === "") {
getInitialResults();
} else {
getResponse(normalizedText);
}
},
800,
{ leading: false }
);
React.useEffect(() => {
if (open) {
updates(search);
}
}, [search, open, currentTab]);