[405 error] 디버깅이라고 쓰고 오류일지라고 읽기

Pyotato·2023년 12월 15일
0

[오류 일기]

목록 보기
1/1

prisma + postgres+ nextjs 풀스택 예제 : https://hyemin-pyo-blog-nextjs-prisma-2-pyotato.vercel.app/

  • postgres 사용도 해보고 싶었고, 풀스택 프로젝트도 해보고 싶다가 nextjs CRUD 프로젝트를 간단하게 따라할 수 있는 nextAuth도 사용하는 예제가 나와 있어서 시도해봤다.

  • nodejs 버젼이 안맞아서 버젼업그레이드를 해야했다던가, 라이브러리들간의 버전에 따른 디펜덴시가 맞지 않아서 재설치를 하고 버젼을 맞춰야한다던가, 타입스크립트 에러등 뭔가 쉬운 예제지만 어찌저찌 배포까지 했는데

  • 이상하게 POST method (새글 create)만 안되는 현상이 발생

  • R,U,D (GET,PUT,DELETE) 메소드들은 다 잘 되는데 왜 POST만 405가 뜨는가?

  • 결론부터 말하자면, vercel에 배포했을때 log를 확인하고서 해결을 할 수 있었다.

    • 아래의 400와 405에러에서 문제가 발생했다.
    • POST /api/auth/session으로 해당 세션에 로그인한 유저의 이메일을 가져오려고 한다.
    • POST /api/post로 제목과 내용, 작성자 이메일을 body에 담아 post를 하려는 부분이다.
      vercel 에러 로그
  • 백쪽에서 POST 요청을 받으면 prisma에서 create를 해주는 예제 코드에서는 다음과 되어 있다.

    • 문제는 위의 에러 로그에서 봤던 거와 같이 getSession은 get요청으로 create post요청을 보내기 전에 먼저 받아와야하는 데이터다. 하지만 아래의 코드에서 잘못된 요청 method으로 세션에 대한 데이터를 받아오려고 했고, 따라서 이메일은 undefined가 되어버린다.
  • 작성자에 대한 이메일이 undefined이 되므로 post도 실패하게 된 것이다.

// pages/post/index.ts
import { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import prisma from "../../../lib/prisma";
// POST /api/post
// Required fields in body: title
// Optional fields in body: content
export default async function handle(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === "POST") {
    const session = await getSession({ req });
    const { title, content } = req.body;
    // const { title, content, email } = req.body;  // 수정한 코드
    const post = await prisma.post.create({
      data: {
        title: title,
        content: content,
        // author: { connect: { email: email } }, // 수정한 코드
        author: { connect: { email: session?.user?.email } },
      },
    });
    res.json(post);
  } else {
    console.log("req method : ", req.method);
  }
}
  • 아래의 코드는 클라이언트 쪽에서 /create 페이지에서 포스트 등록 요청을 하는 코드를 포함한 페이지다.
// pages/create.tsx

import { getSession } from "next-auth/react";
import Router from "next/router";
import React, { useState } from "react";
import Layout from "../components/Layout";

const Draft: React.FC = () => {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");

  const submitData = async (e: React.SyntheticEvent) => {
    e.preventDefault();
    try {
      const session = await getSession();
      // const body = { title, content, email: session?.user?.email }; // 수정한 코드
      const body = { title, content };
      await fetch(`/api/post`, {
        // await fetch(`/api/create`, {
        method: "POST",
        // method: "OPTIONS",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
      });
      await Router.push("/drafts");
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <Layout>
      <div>
        <form onSubmit={submitData}>
          <h1>New Draft</h1>
          <input
            autoFocus
            onChange={(e) => setTitle(e.target.value)}
            placeholder="Title"
            type="text"
            value={title}
          />
          <textarea
            cols={50}
            onChange={(e) => setContent(e.target.value)}
            placeholder="Content"
            rows={8}
            value={content}
          />
          <input disabled={!content || !title} type="submit" value="Create" />
          <a className="back" href="#" onClick={() => Router.push("/")}>
            or Cancel
          </a>
        </form>
      </div>
      <style jsx>{`
        .page {
          background: var(--geist-background);
          padding: 3rem;
          display: flex;
          justify-content: center;
          align-items: center;
        }

        input[type="text"],
        textarea {
          width: 100%;
          padding: 0.5rem;
          margin: 0.5rem 0;
          border-radius: 0.25rem;
          border: 0.125rem solid rgba(0, 0, 0, 0.2);
        }

        input[type="submit"] {
          background: #ececec;
          border: 0;
          padding: 1rem 2rem;
        }

        .back {
          margin-left: 1rem;
        }
      `}</style>
    </Layout>
  );
};

export default Draft;
  • 따라서 위의 예제코드에서 2가지를 수정했을 때 제대로 작동하게 된다.
    • /create 페이지에서 const body = { title, content, email: session?.user?.email };로 바디에 먼저 현재 사용자의 세션 (GET요청)의 이메일을 담아줘서 백엔드로 보내기
// pages/create.tsx
import { getSession } from "next-auth/react";
import Router from "next/router";
import React, { useState } from "react";
import Layout from "../components/Layout";

const Draft: React.FC = () => {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");

  const submitData = async (e: React.SyntheticEvent) => {
    e.preventDefault();
    try {
      const session = await getSession();
      const body = { title, content, email: session?.user?.email };
      await fetch(`/api/post`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
      });
      await Router.push("/drafts");
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <Layout>
      <div>
        <form onSubmit={submitData}>
          <h1>New Draft</h1>
          <input
            autoFocus
            onChange={(e) => setTitle(e.target.value)}
            placeholder="Title"
            type="text"
            value={title}
          />
          <textarea
            cols={50}
            onChange={(e) => setContent(e.target.value)}
            placeholder="Content"
            rows={8}
            value={content}
          />
          <input disabled={!content || !title} type="submit" value="Create" />
          <a className="back" href="#" onClick={() => Router.push("/")}>
            or Cancel
          </a>
        </form>
      </div>
      <style jsx>{`
        .page {
          background: var(--geist-background);
          padding: 3rem;
          display: flex;
          justify-content: center;
          align-items: center;
        }

        input[type="text"],
        textarea {
          width: 100%;
          padding: 0.5rem;
          margin: 0.5rem 0;
          border-radius: 0.25rem;
          border: 0.125rem solid rgba(0, 0, 0, 0.2);
        }

        input[type="submit"] {
          background: #ececec;
          border: 0;
          padding: 1rem 2rem;
        }

        .back {
          margin-left: 1rem;
        }
      `}</style>
    </Layout>
  );
};

export default Draft;
  • POST로 받아왔을 때 prisma에 해당 요청을 보내기
// pages/post/index.ts
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "../../../lib/prisma";
// POST /api/post
// Required fields in body: title
// Optional fields in body: content
export default async function handle(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === "POST") {
    const { title, content, email } = req.body;
    const post = await prisma.post.create({
      data: {
        title: title,
        content: content,
        author: { connect: { email: email } },
      },
    });
    res.json(post);
  } else {
    console.log("req method : ", req.method);
  }
}
  • 로그에 POST 요청이 잘 완성된 모습을 볼 수 있다 (속시원)

😅 위의 코드에서는 그냥 다른 메소드에 대한 처리를 console.log("req method : ", req.method); 해줬는데, 사실 에러 메세지를 좀 더 정제해서 보내줬다면 디버깅이 쉬웠을 거 같다.

  • 예를 들어 클라이언트 쪽에서 서버가 필요한 데이터가 누락되어 있다면 요청을 보내지 않고 먼저 경고를 준다던가, 누락된 이메일의 상태라면 상태코드를 401로 보내주던가?
  • 사실 getSession을 POST요청을 했을 때 같이 하려고 했기 때문에 발생한 문제였던 거 같아서 애초에 헛발질을 많이 했던 거 같다.
  • 처음에는 config 문제인줄 알고 next.config.js 설정이나 vercel.json설정을 해줬었으나, 문제 원인을 건드리는 설정들은 아니었고..vercel 공식문서에서 405대응책은 curl을 이용해서 요청을 보내보는 것이었지만 해결 X, 마지막 보루였던 postman에서도 405에러..
  • 뭔가 바디 파싱이 제대로 안됐나? 싶어서body-parser 라이브러리를 사용해서 해봤지만 안됨.
  • 결국은 코드에 문제가 있었던 것...😅
profile
https://pyotato-dev.tistory.com/ 로 이사중 🚚💨🚛💨🚚💨

0개의 댓글

관련 채용 정보