WEB / FIELD NOTES

POST로 보냈는데 다음 주소는 왜 GET일까

가상 주문 POST /orders가 301이나 302를 만나면 브라우저는 본문 없는 GET으로 따라갑니다. 303은 그 전환이 목적이고, 307과 308은 메서드를 유지합니다.

주문을 저장하는 주소는 https://shop.example/orders 입니다. 메서드는 POST이고, Content-Type은 application/json이며, 본문은 {"sku":"MUG-1","qty":1}입니다. 응답이 302이고 Location이 https://shop.example/orders/1001이면, 브라우저가 그 주소를 따라갈 때 다음 요청은 본문 없는 GET이 됩니다. 같은 Location을 307로 주면 다음 요청은 POST로 남습니다. shop.example과 MUG-1, 수량 1은 가상 예입니다. 이 글을 쓰면서 그 주소에 요청을 보내거나 브라우저 로그를 모으지는 않았습니다.

기준은 2026-09-26에 읽은 RFC 9110과 Fetch 표준입니다. RFC는 사용자 에이전트가 바꿀 수 있는 범위를 말하고, Fetch는 redirect 모드가 follow일 때 브라우저가 따르는 절차를 정합니다. 따로 정하지 않으면 follow입니다. curl처럼 Fetch를 구현하지 않는 클라이언트는 같은 상태 코드에서도 메서드를 유지할 수 있습니다.

영구와 임시, 메서드 유지는 다른 축입니다

301 Moved Permanently는 대상 자원에 새 영구 주소가 생겼고, 이후 참조는 그 주소를 써야 한다는 뜻입니다. 308 Permanent Redirect도 영구입니다. 302 Found와 307 Temporary Redirect는 잠시 다른 주소에 있으므로, 이후 요청도 원래 주소를 계속 씁니다.

301과 302에는 역사적 예외가 있습니다. 사용자 에이전트는 다음 요청의 메서드를 POST에서 GET으로 바꿀 수 있습니다. 그 변경을 원하지 않으면 영구는 308, 임시는 307을 씁니다. 307과 308은 자동으로 따라갈 때 메서드를 바꾸면 안 됩니다.

303 See Other는 같은 자원이 이사했다는 안내가 아닙니다. Location의 주소는 원래 대상과 동등하지 않고, 사용자 에이전트는 그 주소를 GET 또는 HEAD로 조회할 수 있습니다. POST의 결과를 따로 가리키고, 그 결과를 북마크하거나 캐시할 수 있는 주소로 보낼 때 주로 씁니다.

경험적 캐시 목록에는 301과 308이 들어 있고 302, 303, 307은 없습니다. 목록에 없는 코드도 Cache-Control 같은 명시적 신선도가 있으면 저장할 수 있습니다. 다만 POST 응답은 메서드 규칙이 우선합니다. 명시적 신선도가 있고 Content-Location이 그 POST의 주소와 같을 때만 캐시할 수 있으며, 그때도 나중 요청은 GET이나 HEAD입니다. 301이라는 이유만으로 POST 응답이 저장되지는 않습니다.

301은 영구이고 POST를 GET으로 바꿀 수 있다. 302는 임시이고 같은 변경이 가능하다. 303은 다른 자원을 GET 또는 HEAD로 조회한다. 307은 임시, 308은 영구이며 자동으로 따라갈 때 메서드를 유지한다. 301과 308만 경험적 캐시 목록에 있다.
영구인지, 메서드를 바꾸는지, 경험적 캐시 목록에 있는지는 세 가지 질문입니다.

브라우저가 따라가면 302의 POST는 GET이 됩니다

follow이면 Location을 그 응답을 받은 요청 주소 기준으로 다시 요청합니다. error는 네트워크 오류이고, manual은 페이지 탐색이 아니면 따라가지 않습니다. 안전하지 않은 메서드를 자동으로 따라갈 때는 주의가 필요하지만, fetch의 기본은 follow입니다.

상태가 301 또는 302이고 메서드가 POST이면, Fetch는 메서드를 GET으로 바꾸고 본문을 비웁니다. 앞의 302를 follow로 따르면 다음 요청은 본문 없는 GET https://shop.example/orders/1001입니다. 상품 코드와 수량은 그 요청에 없습니다.

같은 Location의 307이나 308은 이 조건에 들어가지 않습니다. 메서드는 POST로 남고, 본문을 다시 읽을 수 있으면 그 본문을 다시 붙입니다. RFC가 301과 302에서 허용한 변경을, Fetch의 follow는 적용합니다. 그 허용을 모든 클라이언트의 의무로 읽지는 않습니다.

가상 POST https://shop.example/orders 의 본문은 sku MUG-1, 수량 1이다. 302 Location /orders/1001 을 따라가면 GET이 되고 본문과 Content-Type은 사라진다. 같은 Location의 307은 POST와 본문을 유지한다.
같은 가상 주문도 302의 다음 요청과 307의 다음 요청은 다릅니다.

303은 결과를 보여 주는 GET입니다

303은 POST만의 규칙이 아닙니다. 메서드가 GET이나 HEAD가 아니면 Fetch는 메서드를 GET으로 바꾸고 본문을 버립니다. PUT이나 DELETE도 303을 follow로 따르면 다음은 GET입니다.

301과 302는 범위가 더 좁습니다. Fetch가 GET으로 바꾸는 것은 그 두 코드의 POST뿐입니다. 가상 PUT https://shop.example/orders/1001이 302로 다른 주소를 가리켜도, follow의 다음 요청은 PUT입니다.

303의 Location은 원래 자원과 같은 자리가 아닙니다. 주문을 만든 POST 다음에 303으로 /orders/1001을 주면, 그 주소는 결과를 가리키고 그 페이지를 다시 열어도 POST를 반복하지 않습니다. 302로 같은 모양을 만들면 메서드는 바뀔 수 있지만, 다른 자원을 보여 주라는 정의는 아닙니다.

303의 Location https://shop.example/orders/1001 은 원래 주소와 동등하지 않고 다음은 본문 없는 GET이다. 302는 같은 자원의 임시 이동이고 POST를 GET으로 바꾸는 것은 역사적 허용이다. Fetch는 301과 302에서 POST만 GET으로 바꾸고, 303은 PUT과 DELETE도 GET으로 바꾼다.
결과를 보여 주려면 303을 쓰고, POST를 유지하려면 307이나 308을 씁니다.

따라갈 때 헤더와 본문이 어떻게 남는지

메서드가 GET이나 HEAD로 바뀌면 Fetch는 Content-Encoding, Content-Language, Content-Location, Content-Type을 지웁니다. RFC 9110은 그때 Content-Length와 Digest 같은 본문 관련 필드도 빼라고 합니다. 두 문서가 지우는 이름 목록은 같지 않습니다. 이 가상 예에서는 Content-Type application/json이 빠집니다.

출처가 바뀌면 Authorization도 빠집니다. https://shop.example에서 https://pay.example로 가면 이 단계에서 지우는 이름은 Authorization입니다. 같은 출처의 /orders/1001이면 여기서 Authorization을 지우지 않습니다. 쿠키는 자격 증명 모드에 따라 나중에 다시 붙을 수 있어 Authorization과 같이 다루지 않습니다. 다른 출처의 JSON 요청이 막히는 조건은 브라우저가 JSON 요청을 막았는데 서버에는 왜 안 보일까에서 따로 봅니다.

303이 아닌데 본문이 있고 그 본문을 다시 읽을 수 없으면 Fetch는 네트워크 오류를 반환합니다. 한 번만 흐르는 스트림을 307로 따라가면 실패할 수 있습니다. 303은 본문을 버리므로 그 검사에서 빠집니다. 리다이렉트 횟수는 0에서 시작하고, 응답을 20번 따라간 다음 리다이렉트는 네트워크 오류입니다. 예전 권고는 최대 다섯 번이었고, 일부 클라이언트는 여전히 고정 한도를 쓸 수 있습니다. Location이 http나 https가 아니면 따라가지 않습니다.

GET으로 바뀔 때 Fetch가 지우는 이름은 Content-Encoding, Content-Language, Content-Location, Content-Type이다. shop.example에서 pay.example로 가면 Authorization이 빠지고 같은 출처면 유지된다. 303이 아니면 본문을 다시 읽을 수 있어야 하고, 스무 번 따라간 다음 리다이렉트는 오류다.
메서드가 바뀌면 본문 헤더가 빠지고, 출처가 바뀌면 Authorization이 빠집니다.

상태 코드만으로 다음 주문을 정하지 않습니다

로그에는 첫 상태와 Location, 두 번째 메서드와 본문 유무, 두 주소의 출처를 같이 적습니다. 두 번째 요청이 GET이라는 것만으로 서버가 POST를 거절했다고 보지 않습니다. 이 갈림은 표준의 절차이고, 가상 주소를 브라우저에서 다시 실행한 결과는 아닙니다.

END OF NOTE목록으로
COMMENTS BOX

이 기록에 대화를 더해 주세요.

궁금한 점, 다른 접근, 직접 해 본 결과를 나눠 주세요.

최신순

로그인 상태 확인 중…

댓글을 불러오는 중…