Dev Notes
Web Vitals
Web Vitals
Dev Notes
DocsHTTP GET/DELETE 요청에 Body를 넣으면 안 되는 이유
Web Vitals
Web Vitals
GitHub
Web26분

HTTP GET/DELETE 요청에 Body를 넣으면 안 되는 이유

RFC 스펙, 클라이언트/서버 호환성 문제, 그리고 올바른 대안

2025년 12월 5일
httprest-apibest-practices

API 스펙을 전달받았는데, GET 요청에 필터 조건을 request body로 보내달라는 내용이었습니다.

GET /api/products
Content-Type: application/json

{
  "category": "electronics",
  "priceRange": { "min": 10000, "max": 50000 },
  "tags": ["sale", "new"]
}

"Query string이 복잡해지니까 body로 받는 게 깔끔하지 않나요?"라는 이유였습니다. 언뜻 보면 합리적인 것 같지만, 이 방식은 실제로 동작하지 않는 환경이 많습니다. 브라우저의 Fetch API는 GET body를 넣으면 TypeError를 던지고, Axios는 아예 지원하지 않습니다.

이 문서에서는 왜 그런지, RFC 스펙은 무엇을 말하는지, 그리고 어떤 대안이 있는지 정리했습니다.

RFC 스펙: "금지"가 아닌 "정의되지 않음"

HTTP 스펙 문서를 직접 찾아보면, GET이나 DELETE 요청에 body를 넣는 것이 명시적으로 "금지"된 것은 아닙니다. 다만 "no defined semantics", 즉 의미가 정의되지 않았다고 표현합니다.

2022년에 발표된 최신 HTTP 표준인 RFC 9110 Section 9.3.1은 GET 요청의 body에 대해 이렇게 말합니다:

"Although request message framing is independent of the method used, content received in a GET request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack."

더 중요한 것은 클라이언트에 대한 명시적인 권고입니다:

"A client SHOULD NOT generate content in a GET request unless it is made directly to an origin server that has previously indicated, in or out of band, that such a request has a purpose and will be adequately supported."

RFC 7231(2014)의 "might cause some implementations to reject"라는 표현에서 RFC 9110은 SHOULD NOT이라는 더 강한 경고와 함께 "request smuggling attack" 가능성까지 언급하고 있습니다.

no defined semantics의 실제 의미

금지(forbidden)된 것은 아니지만, 서버가 body를 무시해도 되고, 거부해도 됩니다. 어떤 동작을 할지 보장이 없다는 뜻입니다. 참고로 유일하게 body가 명시적으로 금지된 메서드는 TRACE뿐입니다.

그런데 Elasticsearch는 GET body를 쓰는데요?

맞습니다. Elasticsearch의 _search API는 GET 요청에 JSON body로 쿼리를 받습니다. 이는 복잡한 검색 조건을 URL에 담기 어렵기 때문인데, 바로 이 때문에 브라우저에서 직접 Elasticsearch를 호출할 수 없고 백엔드 프록시가 필요합니다. Elasticsearch 측에서도 POST를 대안으로 지원합니다.

스펙이 이렇게 모호한 이유가 있습니다. HTTP는 수십 년에 걸쳐 발전해왔고, 다양한 구현체들이 이미 존재하는 상황에서 하위 호환성을 깨뜨리지 않으면서도 올바른 방향을 제시해야 했기 때문입니다.

실제로 겪게 되는 문제들

하지만 "정의되지 않음"이 "해도 된다"는 의미는 아닙니다. 실제 환경에서는 다양한 문제가 발생합니다.

브라우저 Fetch API가 에러를 던짐

가장 흔히 마주치는 문제입니다. 브라우저의 Fetch API로 GET 요청에 body를 넣으면, 조용히 무시하는 게 아니라 TypeError를 던집니다. Fetch 표준 스펙에서 GET/HEAD 메서드에 body가 있으면 에러를 발생시키도록 정의하고 있기 때문입니다.

JavaScript
// ❌ TypeError: Failed to execute 'fetch': Request with GET/HEAD method cannot have body.
fetch('/api/data', {
  method: 'GET',
  body: JSON.stringify({ query: 'test' })
});

DELETE의 경우는 Fetch API에서 body 전송이 허용됩니다. 다만 이후에 설명할 프록시나 서버 프레임워크에서 문제가 발생할 수 있습니다.

왜 Fetch API는 RFC보다 더 엄격할까요? WHATWG Fetch 스펙 메인테이너에 따르면, "예상치 못한 서버에 데이터를 전송하는 것을 방지"하기 위한 보안적 결정입니다. 브라우저는 사용자를 보호해야 하므로 RFC의 "SHOULD NOT"보다 더 강하게 "MUST NOT"으로 구현한 것입니다.

Axios가 GET body를 지원하지 않음

프론트엔드에서 가장 많이 쓰이는 HTTP 클라이언트인 Axios는 GET 요청의 body를 아예 지원하지 않습니다. data 옵션을 넣어도 무시됩니다.

JavaScript
// ❌ Axios는 GET의 data를 무시함
axios.get('/api/data', {
  data: { query: 'test' }  // 무시됨
});

Axios 공식 문서에도 명시되어 있습니다: "data is the data to be sent as the request body. Only applicable for request methods 'PUT', 'POST', 'DELETE', and 'PATCH'". GET은 목록에서 빠져있습니다.

DELETE의 경우는 Axios가 지원하긴 하지만, 일반적인 방식으로는 동작하지 않습니다.

JavaScript
// ❌ 이렇게 하면 안 됨
axios.delete('/api/items/1', { reason: 'duplicate' });
 
// ✅ data 속성을 명시적으로 사용해야 함
axios.delete('/api/items/1', {
  data: { reason: 'duplicate' }
});

XMLHttpRequest는 조용히 무시함

Axios가 내부적으로 사용하는 XMLHttpRequest도 GET body를 지원하지 않습니다. XMLHttpRequest 스펙에서 명시적으로 정의하고 있습니다:

"If the request method is GET or HEAD, the argument is ignored and request body is set to null."

Fetch API와 달리 에러를 던지지 않고 조용히 body를 null로 설정합니다. 에러가 발생하지 않으니 개발자가 문제를 인지하기 더 어렵습니다.

프록시와 CDN이 body를 제거함

클라이언트에서 body를 보내더라도, 중간에 있는 프록시나 CDN이 이를 제거할 수 있습니다. GET 요청은 캐시되는 것이 일반적인데, 캐시 키에는 URL과 헤더만 포함되고 body는 포함되지 않습니다. 이로 인해 다른 body를 보냈는데 같은 캐시 응답이 반환되는 문제가 발생할 수 있습니다.

AWS API Gateway, Azure API Management, Kong 같은 API 게이트웨이들도 GET/DELETE + body 조합을 거부하는 경우가 있습니다.

서버 프레임워크가 파싱하지 않음

서버까지 body가 무사히 도착했다 하더라도, 서버 프레임워크가 이를 파싱하지 않을 수 있습니다.

JavaScript
// Express.js 예시
app.get('/api/data', (req, res) => {
  console.log(req.body);  // {} 또는 undefined
  // body-parser가 GET 요청의 body는 파싱하지 않을 수 있음
});

Spring Boot, Django 등 다른 프레임워크들도 기본 설정에서는 GET body를 파싱하지 않는 경우가 많습니다.

환경별 동작 요약

다양한 환경에서의 동작을 정리하면 다음과 같습니다.

클라이언트

클라이언트GET + bodyDELETE + body
Fetch API❌ TypeError✅ 전송됨
Axios (브라우저)❌ 무시됨⚠️ data 속성 필요
XMLHttpRequest❌ 무시됨 (null)✅ 전송됨
cURL✅ 전송됨✅ 전송됨
Postman✅ 전송됨✅ 전송됨

cURL이나 Postman에서는 body가 전송되기 때문에, 개발 중에는 문제가 없다가 실제 브라우저 환경에서 문제가 발견되는 경우가 많습니다.

서버/인프라

환경GET body 파싱DELETE body 파싱
Express.js⚠️ 기본 미지원✅ 대부분 작동
Spring Boot⚠️ 설정 필요✅ 대부분 작동
Django❌ 기본 미지원⚠️ DRF 설정 필요
nginx (프록시)⚠️ 제거될 수 있음⚠️ 제거될 수 있음
AWS API Gateway❌ 거부⚠️ 설정에 따라 다름

올바른 대안

그렇다면 GET 요청에 복잡한 필터 조건을 전달하거나, DELETE 요청에 추가 정보를 전달해야 할 때는 어떻게 해야 할까요?

GET: Query Parameters 사용

가장 표준적인 방법은 Query Parameters입니다. URL 길이 제한이 있긴 하지만(브라우저마다 다르지만 보통 2,000~8,000자), 대부분의 필터링 요구사항은 충분히 처리할 수 있습니다.

JavaScript
// ✅ 간단한 필터
fetch('/api/users?filter=active&sort=name&page=1');
 
// ✅ 복잡한 필터는 JSON을 URL 인코딩
const params = new URLSearchParams({
  filter: JSON.stringify({ status: 'active', role: 'admin' }),
  sort: 'name',
  page: '1'
});
fetch(`/api/users?${params}`);

DELETE: 상황에 따른 선택

DELETE에 추가 정보를 전달해야 하는 경우, 몇 가지 선택지가 있습니다.

간단한 옵션이라면 Query Parameters나 Headers

JavaScript
// Query Parameters
fetch('/api/items/123?reason=duplicate&force=true', {
  method: 'DELETE'
});
 
// Custom Headers
fetch('/api/items/123', {
  method: 'DELETE',
  headers: {
    'X-Delete-Reason': 'duplicate',
    'X-Force-Delete': 'true'
  }
});

복잡한 데이터라면 POST로 변경

삭제 작업에 복잡한 데이터가 필요하다면, REST 의미론을 약간 희생하더라도 POST를 사용하는 것이 현실적입니다.

JavaScript
// 복잡한 삭제 작업
fetch('/api/items/123/delete', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    reason: 'duplicate',
    force: true,
    notifyUsers: ['user1', 'user2']
  })
});

대량 삭제(Bulk Delete)

여러 항목을 한 번에 삭제하는 경우에도 POST가 더 안정적입니다.

JavaScript
// ✅ 소량이면 Query Parameters
fetch('/api/items?ids=1,2,3,4,5', { method: 'DELETE' });
 
// ✅ 대량이면 POST 엔드포인트
fetch('/api/items/bulk-delete', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ ids: [1, 2, 3, 4, 5, /* ... */] })
});

대안 비교

방법장점단점적합한 경우
Query Params캐시 가능, 호환성 최고URL 길이 제한간단한 필터, 소량 ID
Custom Headers깔끔한 URL비표준, 디버깅 어려움메타데이터 전달
POST로 변경Body 사용 가능REST 의미론 훼손복잡한 작업, 대량 처리

정리

GET/DELETE 요청에 body를 넣는 것은 "금지"된 것은 아니지만, 실제 환경에서 동작을 보장받을 수 없습니다.

핵심 요약

브라우저 Fetch API는 GET body를 넣으면 TypeError를 던지고, Axios는 아예 지원하지 않습니다. 프록시/CDN이 body를 제거하거나 캐시 문제를 일으킬 수 있고, 서버 프레임워크도 기본적으로 파싱하지 않는 경우가 많습니다. Postman이나 cURL에서는 동작하기 때문에 개발 중에 문제를 발견하기 어렵습니다.

GET에 복잡한 필터가 필요하다면 Query Parameters가 가장 안전하고, DELETE에 추가 정보가 필요하다면 상황에 따라 Query Parameters, Headers, 또는 POST 엔드포인트를 고려해볼 수 있습니다.

참고 자료

  • RFC 9110 - HTTP Semantics Section 9.3.1 (GET)
  • WHATWG Fetch Standard
  • WHATWG Fetch Issue #551 - GET/HEAD body restriction
  • Axios Request Config - data option
  • MDN - Using the Fetch API
Written by

Mirunamu (Park Geonwoo)

Software Developer

관련 글 더보기

Web

브라우저 가비지 컬렉션과 메모리 관리

JavaScript 메모리 구조부터 V8 가비지 컬렉터의 동작 원리, 메모리 누수 디버깅까지

읽기
Web

브라우저 렌더링 파이프라인

HTML을 화면에 그리기까지의 전체 과정과 프론트엔드 성능 최적화의 근본 원리

읽기
Web

Core Web Vitals 가이드

Google이 정의한 핵심 사용자 경험 지표(LCP, INP, CLS)와 최적화 전략

읽기
Web

CORS와 Preflight 요청의 동작 원리

Same-Origin Policy부터 Preflight 최적화까지, 브라우저의 교차 출처 요청 제어 메커니즘

읽기
이전 글Core Web Vitals 가이드
다음 글브라우저 렌더링 파이프라인
목차
  • RFC 스펙: "금지"가 아닌 "정의되지 않음"
  • 실제로 겪게 되는 문제들
    • 브라우저 Fetch API가 에러를 던짐
    • Axios가 GET body를 지원하지 않음
    • XMLHttpRequest는 조용히 무시함
    • 프록시와 CDN이 body를 제거함
    • 서버 프레임워크가 파싱하지 않음
  • 환경별 동작 요약
    • 클라이언트
    • 서버/인프라
  • 올바른 대안
    • GET: Query Parameters 사용
    • DELETE: 상황에 따른 선택
    • 대안 비교
  • 정리
  • 참고 자료