cURL은 되는데 Python·fetch 요청은 실패하는 이유

터미널의 cURL로는 응답이 오는데, curlconverter로 옮긴 fetch 코드는 웹페이지에서 데이터를 읽지 못한다. 같은 요청이라던 코드가 어디서 달라졌을까?

cURL은 되는데 Python·fetch 요청은 실패하는 이유
세 줄 요약
  • curlconverter는 cURL의 HTTP 요청을 Python·JavaScript 등으로 옮기는 도구다.
  • 변환 뒤에는 본문 형식, 리다이렉트 기본값, 실행할 브라우저의 CORS 조건을 맞춰야 한다.
  • 코드 생성과 요청 실행은 다른 단계다. 변환한 코드는 실제 요청을 보내고 응답을 대조해야 한다.

문법을 옮겨도 실행 환경의 기본값은 달라진다

cURL로 되던 요청이 변환한 코드에서 실패하면 URL뿐 아니라 본문과 실행 환경부터 대조해야 한다. curlconverter 공식 저장소도 리다이렉트처럼 생성 코드가 해당 런타임의 기본값을 따르는 차이를 명시한다. HTTP 요청 변환을 지원하며 모든 cURL 옵션을 같은 동작으로 옮기는 것은 아니다.

시작 절차는 짧다. Chrome DevTools의 Network에서 대상 요청을 우클릭하고 Copy → Copy as cURL을 선택한다. curlconverter 입력창에 붙인 뒤 Python 또는 JavaScript 출력을 고른다. 프로젝트는 입력 명령과 생성 결과를 전송·기록하지 않고 브라우저 안에서 변환한다고 설명한다.

복사한 요청에는 Cookie나 Authorization 같은 인증 정보가 포함될 수 있다. 공개 질문에 붙일 때는 그 값을 가리고, 코드에서 어떤 계정으로 요청할지 따로 정해야 한다. 브라우저의 로그인 상태를 아무 Python 프로세스나 자동으로 공유하는 것은 아니다.

같은 값도 JSON·폼·쿼리에 넣으면 다른 요청이다

Python Requests에서 딕셔너리를 data=로 넘기면 폼 인코딩이 되고, json=으로 넘기면 JSON으로 직렬화된다. URL의 쿼리 문자열은 params=다. Requests 문서의 구분을 원래 요청의 Content-Type과 맞춰야 한다.

원래 요청의 데이터 위치·형식Python에서 대응할 항목
URL 뒤의 검색·필터 값params
JSON 본문json
폼 인코딩 본문딕셔너리 형태의 data

예를 들어 JSON API에 수량을 보내는 다음 코드는 설명용 예제다. api.example.com은 실제 서비스 주소로 바꿔야 한다.

import requests

response = requests.post(
    "https://api.example.com/items",
    json={"quantity": 2},
    timeout=10,
)
response.raise_for_status()

이미 직렬화한 문자열을 data로 넘기는 방식도 있지만 Content-Type을 별도로 맞춰야 한다. json과 data를 함께 넘기면 json이 무시되므로 두 방식을 겹쳐 쓰지 않는다. 예제의 timeout은 전체 작업을 반드시 10초 안에 끝낸다는 뜻은 아니다.

다른 출처로 보내는 fetch는 CORS 허용이 필요하다

같은 HTTP 요청을 웹페이지의 fetch로 보내면 브라우저의 교차 출처 규칙이 적용된다. MDN의 Fetch 설명에 따르면 단순 요청도 서버가 맞는 CORS 응답 헤더를 보내야 JavaScript가 응답을 읽을 수 있다. 사전 요청이 필요한 경우에는 그 허용을 받아야 실제 요청이 나간다.

mode: "no-cors"는 JSON 응답을 읽기 위한 해결책이 아니다. 이 모드의 교차 출처 응답은 opaque여서 JavaScript가 본문과 헤더를 읽을 수 없다. 브라우저가 직접 관리하는 금지 요청 헤더도 있으므로, cURL에 있던 헤더를 전부 fetch 코드에 복사한다고 같은 요청이 되지는 않는다.

따라서 변환 문법이 맞는데 브라우저에서만 실패한다면 실행 페이지의 출처와 대상 서버의 CORS 설정을 함께 봐야 한다. Python에서의 성공과 웹페이지에서 응답을 읽을 수 있는지는 따로 확인할 항목이다.

리다이렉트와 파일은 실행할 쪽에서 다시 맞춘다

curl은 기본적으로 HTTP 리다이렉트를 따라가지 않으며 -L로 이를 켤 수 있다. Requests의 일반 요청은 기본적으로 리다이렉트를 따라가므로, 처음 응답을 관찰하려는 작업에서는 allow_redirects=False 같은 명시적 설정이 필요하다. curlconverter도 정책을 지정하지 않았을 때 이런 기본값 차이가 남는다고 설명한다.

--data @filename은 파일 내용을 코드에 통째로 넣는다는 뜻도 아니다. 생성 코드는 해당 파일을 읽도록 만들어지므로 실행할 컴퓨터에 파일과 맞는 경로가 있어야 한다. 복잡한 셸 문법 역시 지원 범위를 벗어날 수 있다.

변환 뒤에는 메서드·전체 URL·본문 형식을 맞추고, 브라우저에서 실행한다면 CORS를, 다른 런타임으로 옮긴다면 리다이렉트와 파일 입력을 확인하는 순서가 유용하다. 생성된 코드는 요청을 옮기는 출발점이며, 원본과 같은 조건으로 실행되는지 판단할 근거는 실제 요청과 응답이다.