빌드노트
Location: next가 예상과 다른 경로로 이동하는 이유
Location: next는 응답을 받은 요청 URL을 기준으로 해석한다. 끝 슬래시 유무에 따른 실제 후속 GET 경로와 쿼리 교체 결과를 비교하고, 상대 경로·루트 상대 경로·절대 URL을 고르는 기준을 설명한다.
GET /area/item의 응답에 Location: next를 넣었는데 /next 대신 /area/next로 이동한다면, 상대 주소의 기준을 루트로 생각했는지 확인한다. 상대 Location의 기준은 그 이동 응답을 받은 요청 URL이다. 사이트 루트의 /next로 보내려면 Location: /next를 써야 한다.
RFC 9110의 Location 정의 (새 탭에서 열림)는 상대 참조를 해당 요청의 target URI를 기준으로 해석하도록 정한다. 이동이 여러 번 이어진다면 각 응답을 받은 요청 URL이 기준이다. 최초에 입력한 주소를 끝까지 기준으로 삼는 것은 아니다.
마지막 슬래시 하나로 next의 목적지가 달라진다
고정 응답만 보내는 loopback 서버에서 /area/item과 /area/item/에 같은 302와 Location: next를 반환했다. Windows, Node.js 22.23.2, curl 8.21.0에서 확인했으며 최초 요청과 후속 요청은 모두 GET이었다. 아래는 curl의 상세 기록에서 요청 줄·상태·Location만 추린 것이다.
> GET /area/item HTTP/1.1
< HTTP/1.1 302 Found
< Location: next
> GET /area/next HTTP/1.1
< HTTP/1.1 200 OK
> GET /area/item/ HTTP/1.1
< HTTP/1.1 302 Found
< Location: next
> GET /area/item/next HTTP/1.1
< HTTP/1.1 200 OK
next처럼 슬래시 없이 시작하는 경로는 기준 URL 경로의 마지막 슬래시 뒤를 대체한다. /area/item에서는 /area/ 뒤에 붙고, /area/item/에서는 마지막 슬래시까지 남아 /area/item/ 뒤에 붙는다. 서버가 item을 파일로 취급하는지, 디렉터리나 컨트롤러로 취급하는지는 이 계산의 조건이 아니다. URL에 적힌 경로가 기준이다. RFC 3986의 경로 결합 규칙 (새 탭에서 열림)
같은 두 경로에서 Location: /next로 바꾸자 후속 요청은 둘 다 GET /next였다. 앞의 슬래시가 현재 경로 대신 같은 origin, 즉 scheme·호스트·포트의 루트를 선택한다. 애플리케이션이 /area 아래 배치되어 있어도 /next가 자동으로 /area/next가 되지는 않는다.
?page=2는 기존 쿼리에 page만 추가하지 않는다
기존 쿼리가 있는 요청도 비교했다. 두 경로에 각각 ?sort=old&page=1을 붙여 요청한 뒤, 세 가지 Location을 반환했을 때 서버에 도착한 후속 요청은 다음과 같았다. 왼쪽은 최초 요청의 경로와 쿼리, 가운데는 응답 헤더 값, 오른쪽은 다음 요청의 경로와 쿼리다.
/area/item?sort=old&page=1 | next -> /area/next
/area/item/?sort=old&page=1 | next -> /area/item/next
/area/item?sort=old&page=1 | /next -> /next
/area/item/?sort=old&page=1 | /next -> /next
/area/item?sort=old&page=1 | ?page=2 -> /area/item?page=2
/area/item/?sort=old&page=1 | ?page=2 -> /area/item/?page=2
next와 /next에서는 기존 쿼리가 사라졌다. ?page=2에서는 경로와 끝 슬래시는 유지됐지만, 기존 쿼리 전체가 page=2로 교체되어 sort=old가 사라졌다. page 키 하나만 갱신하고 나머지 키를 합치는 동작이 아니다. RFC 3986의 상대 참조 해석 (새 탭에서 열림)과 WHATWG URL의 relative state (새 탭에서 열림)에서 이 경로·쿼리 처리를 확인할 수 있다.
정렬 조건도 유지해야 한다면 서버가 필요한 쿼리를 포함해 목적지를 만들어야 한다. 같은 실험에서 Location: ?sort=old&page=2를 반환하자 두 경로 모두 끝 슬래시를 그대로 두고 sort=old&page=2를 요청했다. 여기서는 고정 문자열로 비교했다. 실제 서비스에서는 유지할 매개변수를 정한 뒤 URL API로 구성한다.
응답을 받은 URL과 Location을 한 쌍으로 확인한다
주소 계산만 확인하려면 서버를 띄울 필요가 없다. Node.js가 설치되어 있다면 터미널에서 node를 실행한 뒤 다음 JavaScript를 붙여 넣는다. 8080은 계산에 사용할 주소의 일부일 뿐이며, 이 코드는 네트워크 요청을 보내지 않는다.
const origin = 'http://127.0.0.1:8080';
for (const path of ['/area/item', '/area/item/']) {
const requestUrl = origin + path + '?sort=old&page=1';
for (const location of ['next', '/next', '?page=2']) {
console.log(path, location, '=>', new URL(location, requestUrl).href);
}
}
Node.js 22.23.2에서 실행한 출력은 다음과 같다. 위의 실제 GET 경로와 같은 결과다.
/area/item next => http://127.0.0.1:8080/area/next
/area/item /next => http://127.0.0.1:8080/next
/area/item ?page=2 => http://127.0.0.1:8080/area/item?page=2
/area/item/ next => http://127.0.0.1:8080/area/item/next
/area/item/ /next => http://127.0.0.1:8080/next
/area/item/ ?page=2 => http://127.0.0.1:8080/area/item/?page=2
Node의 URL 생성자 (새 탭에서 열림)에서 두 번째 인자가 상대 주소를 해석할 기준이다. 자기 응답을 진단할 때는 location에 실제 응답 헤더 값을, requestUrl에 그 응답을 받은 전체 요청 URL을 넣는다. origin만 넘기면 경로와 쿼리가 빠져, 이번 문제를 가린다.
브라우저에서는 Network 기록을 보존하고 해당 3xx의 Request URL과 응답 Location, 바로 다음 요청의 Request URL을 이어서 읽는다. 다음 요청이 다시 3xx이면 그 요청 URL로 기준을 바꿔 확인한다. 파싱한 주소와 실제 후속 요청이 다르다면 다른 중간 이동이 있었는지, 서버나 프록시가 Location을 바꿨는지부터 확인한다. 파싱 계산만으로 실제 헤더가 무엇이었는지 알 수는 없다.
의도한 경로에 맞춰 헤더를 고른다
- 현재 URL의 경로 구조에 맞춰 이동하려면
next같은 상대 경로를 쓴다./area/item과/area/item/에서 목적지가 달라도 의도에 맞는지 확인한다. - 같은 origin에서 고정된 경로로 보내려면
/next처럼 루트 상대 경로를 쓴다. 필요한 경로가/area/next라면 그 전체 경로를 적는다. - scheme·호스트·포트까지 명시해야 한다면 절대 URL을 쓴다. 실험에서는 같은 loopback origin의 전체 URL을 Location에 넣었고, 끝 슬래시가 다른 두 요청 모두
/next로 이동했다. - 같은 경로에서 쿼리만 바꾸려면
?page=2를 쓸 수 있다. 유지해야 하는 다른 쿼리까지 목적지에 포함한다.
이번에 직접 확인한 범위는 같은 loopback origin의 HTTP 요청과 위 고정 Location이다. curl의 자동 추적은 한 번으로 제한했고, 서버 수신 경로와 최종 URL을 함께 비교했다. curl의 Location 옵션 (새 탭에서 열림)은 이동을 따라가게 하며, 횟수는 --max-redirs로 제한할 수 있다. 브라우저에서의 이동, HTTPS 프록시, 프레임워크가 헤더를 만드는 과정은 실행하지 않았다. 요청 메서드 보존과 사용자 입력 목적지의 허용 여부도 이 실험의 판단 범위에 포함하지 않는다.