본문 바로가기
빌드노트 검색

빌드노트

curl을 GET으로 바꿨는데 쿼리 파라미터가 없다

curl -X GET은 -d의 본문을 URL 쿼리로 옮기지 않는다. 로컬 수신기로 메서드·요청 대상·본문을 비교하고, 쿼리 전송에는 -G를, 값 속 구분 문자 보존에는 --data-urlencode를 고르는 방법을 설명한다.

curl -X GET -d "q=pen"으로 요청했는데 서버의 쿼리 파라미터 q가 비어 있다면, 메서드보다 데이터가 놓인 위치를 먼저 본다. -X GET은 요청의 메서드 이름을 바꿀 뿐, -d의 데이터를 URL로 옮기지 않는다. URL 쿼리를 받는 GET API에 보내려면 -X GET을 -G로 바꾼다. 값에 구분 문자가 들어갈 수 있다면 -G --data-urlencode "q=원문값"을 사용한다.

curl의 --request 설명 (새 탭에서 열림)은 메서드를 지정하는 일과 curl의 전송 동작을 구별한다. --get 설명 (새 탭에서 열림)은 -d 등의 데이터를 URL의 쿼리 문자열로 붙인다고 명시한다. 서버가 URL 쿼리에서만 값을 읽는다면, GET 본문에 q=pen이 도착해도 그 쿼리에 q가 생기지는 않는다.

같은 q=pen이 도착한 위치

Windows 11, Windows PowerShell 5.1, curl 8.21.0에서 새 로컬 수신기로 요청을 보냈다. 수신기는 Python 3.12.10의 소켓으로 만들었으며, HTTP/1.1 요청의 메서드·요청 대상 원문·본문 바이트를 따로 저장했다. 여기서 요청 대상은 /search?q=pen처럼 경로와 쿼리를 포함한 부분이다. 쿼리를 해석하기 전에 기록했으므로, 파라미터 파싱 결과와 구별할 수 있다.

자기 API에서 비교하려면 쿼리 q를 받는 로컬 GET 경로를 실행하고, 아래 $url을 그 주소로 바꾼다. 24137은 이번 실험에서 배정된 포트이며 현재 제공되는 테스트 서비스가 아니다. 서버 쪽에서는 쿼리와 폼을 합친 파라미터 목록만 보지 말고, 파싱 전 요청 대상과 본문을 구별해서 확인한다. 응답 내용은 자기 API에 따라 달라진다.

다음 세 명령은 같은 입력을 보내며, 메서드 지정과 쿼리 전송 옵션의 차이를 확인한다. PowerShell에서 curl.exe를 직접 호출한다. 첫 옵션 --disable로 개인 curl 설정을 제외하고, --noproxy "*"로 로컬 요청을 직접 연결했다.

$url = "http://127.0.0.1:24137/search"
curl.exe --disable --noproxy "*" --http1.1 --max-time 4 -sS -X GET -d "q=pen" "$url"
curl.exe --disable --noproxy "*" --http1.1 --max-time 4 -sS -G -d "q=pen" "$url"
curl.exe --disable --noproxy "*" --http1.1 --max-time 4 -sS -G --data-urlencode "q=pen" "$url"

실제 수신 기록에서 필요한 필드만 추리면 다음과 같다. body=""는 읽은 본문이 0바이트라는 뜻이다.

-X GET -d          method=GET  raw_target=/search        body="q=pen" (5바이트)
-G -d              method=GET  raw_target=/search?q=pen  body="" (0바이트)
-G --data-urlencode method=GET  raw_target=/search?q=pen  body="" (0바이트)

세 요청 모두 GET이었지만 첫 요청에만 URL 쿼리가 없었다. 데이터가 사라진 것이 아니라 본문에 남았다. 별도 대조로 -X GET 없이 -d "q=pen"만 보냈을 때는 POST였고, 요청 대상 /search와 본문 q=pen은 첫 요청과 같았다. --data의 기본 HTTP 전송 방식 (새 탭에서 열림)에 메서드 이름만 덮어쓴 결과와 맞는다.

이 수신기는 GET 본문도 읽어 200으로 응답하도록 작성했다. 그렇다고 다른 서버나 프록시도 같은 요청을 지원한다는 뜻은 아니다. RFC 9110의 GET 정의 (새 탭에서 열림)는 GET 본문에 일반적으로 정의된 의미가 없고, 일부 구현은 요청을 거절할 수 있다고 설명한다. 또 원 서버가 용도와 지원을 미리 알린 직접 요청 같은 조건 없이는 클라이언트가 GET 본문을 생성하지 않도록 권고한다. 이번 문제에서는 먼저 API가 요구하는 URL 쿼리에 값을 넣어야 한다.

쿼리로 옮겨도 값 인코딩은 남는다

q=pen에서는 -G -d로 충분했지만, 검색어 자체가 pen&paper라면 다르다. -G가 데이터의 위치를 바꾸어도 -d가 값 속의 &를 자동으로 인코딩해 주지는 않는다. --data-urlencode의 name=content 형식 (새 탭에서 열림)은 = 뒤의 값을 URL 인코딩한다. 이 형식에서 이름 부분은 이미 올바르게 인코딩되어 있어야 하며, 여기서는 단순한 이름 q를 사용한다.

아래 두 명령은 앞에서 지정한 같은 로컬 주소로 원문 검색어를 보내며, 값 안의 &가 보존되는지 비교한다. 따옴표는 셸에 하나의 인자를 전달하는 역할이며 URL 인코딩을 대신하지 않는다.

curl.exe --disable --noproxy "*" --http1.1 --max-time 4 -sS -G -d "q=pen&paper" "$url"
curl.exe --disable --noproxy "*" --http1.1 --max-time 4 -sS -G --data-urlencode "q=pen&paper" "$url"

수신한 요청 대상과 그 쿼리를 Python의 parse_qs(..., keep_blank_values=True)로 읽은 결과는 다음과 같았다. 두 요청의 본문은 모두 0바이트였다.

-G -d              raw_target=/search?q=pen&paper    query={"q":["pen"],"paper":[""]}
-G --data-urlencode raw_target=/search?q=pen%26paper  query={"q":["pen&paper"]}

첫 요청에서는 &가 항목 구분자로 읽혀 q의 값이 pen에서 끝났다. 두 번째는 값 속의 &를 %26으로 표현해 한 번의 쿼리 파싱으로 pen&paper를 얻었다. --data-urlencode에는 미리 %26으로 바꾼 문자열 대신 원문 값을 넘긴다. -G로 쿼리 위치를 정하고 --data-urlencode로 각 값의 표현을 정하는 두 선택이 함께 필요하다.

수정 후에도 q가 없으면 수신한 요청 대상부터 확인한다. ?q=pen 자체가 없다면 실제 실행한 명령과 보낸 URL을 다시 본다. 요청 대상에 ?q=pen이 있는데 애플리케이션에서 읽지 못한다면, 그다음은 파라미터 이름·매핑과 서버의 파싱 경로를 확인할 차례다. 연결 오류나 GET 거절로 수신 기록을 얻지 못했다면 데이터 위치를 비교한 결과로 취급하지 않는다.

이번 확인은 합성 값으로 보낸 로컬 HTTP 요청 6개의 비교다. 모든 curl 요청은 종료 코드 0이었고 수신기는 비교 후 닫았다. 특정 서버 프레임워크의 파라미터 바인딩이나 프록시를 거치는 경로, HTTP/2·HTTP/3의 동작은 실행하지 않았다.

← 전체 글 보기