Content-Type
고친 사람 github-actions[bot]
Content-Type 은 지금 보내는 본문을 무엇으로 읽어야 하는지 받는 쪽에 알려 줍니다. 보내는 쪽이 본문 앞에 형식 이름표를 한 줄 붙입니다. 받는 쪽은 그 이름표대로 바이트를 풉니다. 이름표가 없으면 받는 쪽은 본문이 무엇인지 짐작해서 열게 됩니다.
쉽고 빠른 이해
Content-Type 은 본문에 붙는 형식 이름표입니다. 서버가 Content-Type: text/html 을 붙여
보내면 받는 쪽은 그 본문을 웹 페이지로 그립니다.
이게 없으면 받는 쪽은 같은 바이트를 글로 읽을지 그림으로 그릴지 스스로 짐작해야 합니다. 짐작이 빗나가면 글자가 깨져 나오거나, 글로 올린 파일이 코드로 돌아갑니다.
이렇게 돕니다.
- 본문을 보내는 쪽이 형식 이름을 이 헤더에 적습니다
- 받는 쪽은 그 이름을 보고 본문을 어떻게 풀지 정합니다
- 풀 방법이 없는 형식이면 본문을 읽지 않고 거절합니다
대가는 이름표와 본문이 어긋나도 막아 주는 장치가 없다는 것입니다. 보내는 쪽이 잘못 적으면 받는 쪽은 틀린 이름표를 믿고 본문을 엽니다.
상세
Content-Type 은 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜) 메시지의 헤더 필드입니다. 헤더는 본문 앞에 붙어 이 데이터를 어떻게 다룰지 알려 주는 줄입니다. 이 필드에는 본문이 어떤 형식으로 적혔는지를 가리키는 이름이 들어갑니다.
네트워크를 건너온 본문은 그냥 바이트 덩어리입니다. 같은 바이트를 글로 읽을지, 그림으로 그릴지, 프로그램이 풀어 쓸 데이터로 볼지는 바이트만 봐서는 안 정해집니다. 보내는 쪽이 그 답을 이 한 줄에 적어 본문과 함께 보냅니다.
아래는 주문 하나를 넣는 요청입니다. 본문이 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법)으로 적혔다는 것을 이 헤더가 알립니다.
POST /orders
Content-Type: application/json
{"item": "tea", "qty": 2}
서버는 빈 줄 아래의 바이트를 풀기 전에 Content-Type 줄을 먼저 읽습니다. 그리고 JSON
해석기를 골라 본문을 객체로 되살립니다. 같은 바이트라도 이 값이 text/plain 이었다면 서버는
뜻 없는 글자 뭉치로 받았을 것입니다.
값의 생김새
값은 큰 분류와 구체 형식을 슬래시로 이은 이름 하나입니다. 이 이름을 미디어 타입이라고 부릅니다. 뒤에 세미콜론을 찍고 그 형식에만 필요한 값을 덧붙일 수 있습니다. 이 값을 매개변수라고 부릅니다.
Content-Type: text/html; charset=utf-8
이 한 줄은 두 층으로 갈립니다. 세미콜론 앞이 미디어 타입이고 뒤가 매개변수입니다. 미디어 타입은 다시 슬래시를 경계로 갈립니다.
flowchart TD
V["Content-Type 값<br/>text/html; charset=utf-8"]
V --> M["미디어 타입 · text/html"]
V --> P["매개변수 · charset=utf-8<br/>세미콜론으로 여럿 이을 수 있다"]
M --> T["큰 분류 · text"]
M --> S["구체 형식 · html"]
조각마다 무엇을 말하는지는 이렇습니다.
| 조각 | 무엇을 말하나 | 위 줄에서 |
|---|---|---|
| 슬래시 앞 | 큰 분류 | text · 사람이 읽는 글 |
| 슬래시 뒤 | 구체 형식 | html · 웹 페이지 |
| 세미콜론 뒤 · 매개변수 | 그 형식에만 필요한 값 | charset · 글자 코드 |
큰 분류는 정해진 목록에서 고릅니다. 글은 text, 그림은 image, 소리는 audio, 프로그램이
다루는 데이터는 application 입니다. 새 형식이 생기면 그 분류 아래에 구체 형식 이름을 하나 더
등록합니다.
이름을 이렇게 짓는 방식은 메일에서 먼저 굳었습니다. 글과 첨부 파일을 한 통에 담으려고 만든 MIME(Multipurpose Internet Mail Extensions, 다목적 인터넷 메일 확장)이 형식 이름표를 정했습니다. 웹은 그 이름표와 필드 이름을 그대로 빌려 왔습니다.
글자 코드를 정하는 charset
글을 실어 보내는 형식에는 charset 매개변수가 따라붙습니다. 같은 글자라도 어떤 글자
코드(문자셋)로 적었느냐에 따라 바이트가 달라지기 때문입니다. 받는 쪽은 이 값을 보고
바이트를 글자로 되돌립니다.
이 값이 빠지면 받는 쪽이 글자 코드를 짐작합니다. 짐작이 빗나가면 한글이 알아볼 수 없는 기호로
바뀝니다. 웹에서 오가는 글은 대개 UTF-8(Unicode Transformation Format 8-bit, 유니코드
변환 형식 8비트)으로 적습니다. 그 이름을 이 매개변수에 utf-8 로 적어 보냅니다.
본문을 조각으로 나누는 multipart
파일과 입력값을 한 요청에 같이 보내려면 본문을 여러 조각으로 나눠야 합니다. 이때 큰 분류로
multipart 를 씁니다. 조각과 조각 사이에 끼울 구분 문자열은 boundary 매개변수로 정합니다.
Content-Type: multipart/form-data; boundary=XyZ
받는 쪽은 본문에서 XyZ 가 적힌 줄을 찾아 조각을 가릅니다. 조각은 저마다 자기 Content-Type 을
또 갖습니다. 본문이 이렇게 놓입니다.
flowchart TD
H["바깥 헤더<br/>Content-Type: multipart/form-data; boundary=XyZ"]
B1["--XyZ"]
subgraph P1["조각 1 · 사진"]
C1["Content-Type: image/png"] --> D1["사진 바이트"]
end
B2["--XyZ"]
subgraph P2["조각 2 · 입력값"]
C2["Content-Type: text/plain"] --> D2["이름 값"]
end
E["--XyZ-- · 여기서 본문이 끝난다"]
H --> B1
B1 --> P1
P1 --> B2
B2 --> P2
P2 --> E
바깥 Content-Type 은 본문이 조각으로 나뉘어 있다는 것만 알립니다. 조각 하나하나의 형식은 조각마다 붙은 Content-Type 이 알립니다. 그래서 한 요청 안에서 사진 조각은 그림으로, 이름 조각은 글로 다뤄집니다.
못 다루는 형식의 거절
받는 쪽이 이 이름표를 보고 갈리는 갈래는 셋입니다.
flowchart TD
A["본문이 도착한다"] --> B{"Content-Type 이 붙어 있나"}
B -->|붙어 있다| C{"그 형식을 풀 수 있나"}
B -->|없다| D["받는 쪽이 형식을 짐작한다"]
C -->|풀 수 있다| E["적힌 형식으로 본문을 푼다"]
C -->|못 푼다| F["본문을 안 읽고 거절한다"]
거절은 HTTP 415(Unsupported Media Type, 지원하지 않는 미디어 타입)로 돌아옵니다. JSON 만 받는 서버에 사진을 보내면 이 응답을 받습니다. 서버에 그 형식을 풀 방법이 없다는 뜻이라, 같은 본문을 다시 보내도 답은 같습니다.
요청과 응답에서 적는 쪽
이 필드는 본문을 보내는 쪽이 적습니다. 요청에 본문이 실리면 POST 를 보내는 쪽이 적습니다. 응답에 본문이 실리면 서버가 적습니다.
본문이 없는 요청에는 붙이지 않습니다. 목록을 읽어 오기만 하는 요청처럼 보낼 본문이 없으면 가리킬 형식도 없기 때문입니다.
받고 싶은 형식을 말하는 헤더는 따로 있습니다. Accept 는 요청에 실려 「나는 이런 형식으로 받고 싶다」를 알립니다. 서버는 그중 하나를 골라 응답에 담습니다. 무엇을 골랐는지는 다시 Content-Type 에 적습니다. 이렇게 형식을 맞춰 가는 과정을 콘텐츠 협상이라고 합니다.
| 헤더 | 누가 적나 | 무슨 뜻인가 |
|---|---|---|
| Content-Type | 본문을 보내는 쪽 | 지금 보내는 본문은 이 형식이다 |
| Accept | 요청을 보내는 쪽 | 응답은 이 형식들 중 하나로 달라 |
서버가 요청이 바라는 형식을 하나도 만들 수 없는 때도 있습니다. 그때는 HTTP 406(Not Acceptable, 받아들일 수 없음)으로 답합니다. 한 왕복은 이렇게 오갑니다.
sequenceDiagram
participant C as 클라이언트
participant S as 서버
C->>S: Accept: application/json (이 형식으로 달라)
Note over S: 만들 수 있는 형식 중에서 고른다
S-->>C: Content-Type: application/json (이걸로 보냈다)
Note over C,S: 하나도 못 만들면 406 으로 답한다
이름표와 본문의 어긋남
이 값이 본문과 맞는지 확인해 주는 장치는 없습니다. 보내는 쪽이 적은 대로 받는 쪽이 믿습니다. 그래서 잘못 적힌 이름표는 그대로 오해가 됩니다.
받는 쪽이 이름표 대신 본문 앞머리의 바이트를 들여다보기도 합니다. 파일 앞머리에는 그 형식임을 알리는 고정 바이트가 흔히 들어 있습니다. 이것을 매직 넘버라고 합니다.
이 바이트로 형식을 판정하는 것을 MIME 스니핑이라고 부릅니다. 이름표가 없거나 이름표가 본문과 어긋나 보일 때 브라우저가 이렇게 합니다.
짐작은 보안 문제를 부릅니다. 공격자가 코드를 심은 파일을 글 파일이라며 올립니다. 나중에 다른 사용자가 그 파일을 열 때 브라우저가 내용을 보고 HTML(HyperText Markup Language, 하이퍼텍스트 마크업 언어)로 짐작하면, 심어 둔 코드가 그 사용자 화면에서 돕니다.
이를 막으려고 서버는 X-Content-Type-Options 헤더에 nosniff 를 적습니다. 그러면
브라우저는 짐작하지 않고 이름표대로만 다룹니다. 올릴 때부터 열릴 때까지를 한 번 훑으면
이렇습니다.
sequenceDiagram
participant A as 올린 사람
participant S as 서버
participant B as 브라우저
participant V as 다른 사용자
A->>S: 글 파일이라며 올린다 (안에 코드가 들어 있다)
V->>S: 나중에 그 파일을 연다
S-->>B: 이름표 없이 파일을 내려보낸다
Note over B: 이름표가 없거나 어긋나면 내용을 보고 HTML 로 짐작한다
B-->>V: 심어 둔 코드가 이 화면에서 돈다
Note over S,B: nosniff 가 붙어 있으면 여기서 짐작이 막힌다
관련 항목
이것이 속하는 상위 분류
헤더 · 필드 · 표현 메타데이터 · 메타데이터 · 표현 · HTTP
이 필드에 적히는 값과 그 조각
미디어 타입 · 문자셋 · boundary · 미디어 타입 매개변수 · MIME · 서브타입
받고 싶은 형식을 적는 요청 헤더
Accept · Accept-Charset · Accept-Language · Accept-Encoding · 콘텐츠 협상 · Vary
본문과 나란히 붙는 다른 표현 헤더
Content-Length · Content-Encoding · Content-Language · Content-Disposition · Content-Location
형식이 안 맞을 때 돌아오는 상태 코드
HTTP 415 · HTTP 406 · HTTP 400 · 상태 코드
이 값을 붙여 본문을 보내는 요청 메서드
이 이름표가 가리키는 본문 형식
JSON · HTML · XML · CSV · UTF-8 · multipart-form-data · application-x-www-form-urlencoded
이름표 대신 내용을 짐작할 때 터지는 문제
MIME 스니핑 · X-Content-Type-Options · 크로스 사이트 스크립팅 · 매직 넘버 · 파일 시그니처 · 문자 깨짐
이 필드를 정의하는 표준 문서
RFC 9110 · RFC 2045 · RFC 6838 · IANA 미디어 타입 레지스트리 · HTTP 헤더 필드 레지스트리
다른 이름: Content-Type 헤더 · Content-Type 필드 · 콘텐츠 타입