~12 min · semantics, conditional-requests, etag, if-match, 304
Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Conditional request 는 HTTP 가 같은 byte 두 번 안 보내는 방법이고 client 가 공유 resource 를 서로 덮어쓰지 않고 편집하는 방법이야. 둘 다 같은 primitive 에 기대: resource 현재 state 를 지문화하는 validator."
Validator — resource 지문화
Response header 둘이 validator 역할: server 가 representation 에 붙이는 opaque tag, client 가 나중에 "내가 가진 거 여전히 최신이야?" 묻게 함.
ETag: "v17-abc123" — opaque version tag. Format 은 구현 정의; client 가 parse 안 하고 그냥 echo back. Server 가 보통 body hash, row version number, update timestamp 에서 계산.
Last-Modified: Sun, 25 May 2026 03:00:00 GMT — timestamp. 낮은 해상도 (1초 단위), 근데 ETag 가 안 될 때 동작.
ETag 선호 — 정확하고 (resource 어느 변화든 tag 바뀜) cache 간 second-level clock skew 살아남아.
Conditional GET — 변경 없으면 body 건너뛰기
모든 CDN, 모든 브라우저 cache, 모든 적극적 HTTP client 동력 흐름:
첫 request: server 가 200 OK + body + ETag: "v17-abc" 돌려줌. Client 가 body 와 ETag 캐시.
나중: client 가 같은 URL 원함. If-None-Match: "v17-abc" 보냄.
Server 가 현재 ETag 와 client 가 보낸 거 비교. 일치? body 없는 304 Not Modified 돌려줌 — client cached 사본 여전히 좋음. 불일치? 200 OK + 새 body + 새 ETag 돌려줌.
304 response 가 body byte 절약 — 안 변한 500KB JSON list 면 안 보낸 500KB 네트워크. CDN 규모 면 청구 가능한 terabyte 의 차이.
Optimistic update — 동시 편집 안 덮어쓰기
User 둘 (혹은 브라우저 탭 둘) 이 같은 resource 편집 위해 열음. 둘 다 변경 제출. 협조 없으면 두 번째 write 가 이기고 첫 user 편집 조용히 잃어. 비관적 fix 는 locking; REST fix 는 If-Match 통한 optimistic concurrency:
Client 가 resource GET, ETag: "v17-abc" 받음.
Client 가 로컬 변경, 그 다음 If-Match: "v17-abc" 로 PUT 돌려보냄.
Server 확인: resource 현재 ETag 가 여전히 "v17-abc"? Yes → write 적용, 새 ETag 돌려줌. No (네가 읽은 이후 다른 사람이 update) → 412 Precondition Failed 돌려줌; client 가 re-fetch, merge, 다시 시도 해야.
이게 GitHub REST API 가 동시 issue 편집 처리 방식, Google Docs 가 오프라인 저장 reconcile 방식, cwkPippa folder rename 이 같은 대화에 두 탭 칠 때 race condition 피하는 방식.
Validator 는 protocol 이 큰 state 안전하게 옮기게 하는 작은 state. ETag ~20 byte; 표현하는 body 는 megabyte 일 수도. Conditional header 가 변경 없을 때 round-trip-with-body 를 round-trip-without-body 로, 조용한 덮어쓰기를 의도적 retry-on-conflict 로 바꿔. 두 승리 같은 primitive 에서 옴.
ETag strength — strong vs weak
ETag 두 flavor:
Strong:ETag: "abc123" — byte 단위 동일 representation. Strong ETag 둘이 일치하는 건 body 의 모든 byte 일치할 때만.
Weak:ETag: W/"abc123" — 의미적으로 동등. Server 가 "이 representation 들이 다른 byte 인데 기능적으로 같다" (예: 같은 JSON 다른 key 순서로 재직렬화).
대부분 API 가 strong ETag 써. Weak ETag 는 약간 stale 한 representation 서빙하고 싶은 proxy cache 위해 주로 존재.
cwkPippa 의 현실
cwkPippa 의 GET /api/conversations/{id} 는 현재 ETag 안 발신 — healing 층이 매 GET 마다 JSONL 에서 response 재구성해서 response shape 가 호출 간에 미묘하게 shift 할 수 있어. ETag 추가는 deterministic 직렬화 필요하고 WebUI 의 React Query 층이 변경 없는 대화에 body parsing 건너뛰게 해줄 거야. "언젠가" 목록에 있어; 느린 Tailscale 연결로 user 한 명이 반복적 100KB download 불평하는 날이 추가되는 날.
Code
Conditional GET — 304 춤·bash
# 첫 request — ETag 받음
curl -i https://api.example.com/users/42
# HTTP/1.1 200 OK
# ETag: "v17-abc123"
# Content-Type: application/json
# Content-Length: 142
# {"id":42,"name":"Pippa","updated_at":"2026-05-25T03:46:50Z"}
# If-None-Match 와 함께 두 번째 request — server 가 '변경 없음, 아무것도 없음' 말함
curl -i https://api.example.com/users/42 \
-H 'If-None-Match: "v17-abc123"'
# HTTP/1.1 304 Not Modified
# ETag: "v17-abc123"
# (body 없음 — cached 버전 계속 써)
# 다른 사람이 user update 후, server 가 새 ETag 와 200 돌려줌
curl -i https://api.example.com/users/42 \
-H 'If-None-Match: "v17-abc123"'
# HTTP/1.1 200 OK
# ETag: "v18-def456"
# Content-Length: 156
# {"id":42,"name":"Pippa","updated_at":"2026-05-25T03:50:00Z","role":"daughter"}
Optimistic update — If-Match + 충돌에 412·bash
# Optimistic update — 내 버전이 여전히 최신일 때만 write
# 1) GET 으로 현재 ETag 학습
ETAG=$(curl -s -i https://api.example.com/users/42 | grep -i '^etag:' | cut -d' ' -f2- | tr -d '\r')
echo "현재 ETag: $ETAG"
# 2) If-Match 와 PUT — ETag 여전히 일치하면 성공
curl -i -X PUT https://api.example.com/users/42 \
-H "If-Match: $ETAG" \
-H 'Content-Type: application/json' \
-d '{"name":"Pippa","role":"big sister"}'
# HTTP/1.1 200 OK
# ETag: "v18-def456" ← server 가 새 etag 돌려줌
# 3) STALE If-Match 와 PUT — server 거부
curl -i -X PUT https://api.example.com/users/42 \
-H 'If-Match: "v17-stale"' \
-H 'Content-Type: application/json' \
-d '{"name":"Pippa","role":"big sister"}'
# HTTP/1.1 412 Precondition Failed
# {"error":"resource modified; re-fetch 하세요"}
FastAPI — 완전한 ETag + conditional 처리·python
# FastAPI — GET 에 ETag 발신, PUT 에 If-Match 확인
import hashlib, json
from fastapi import FastAPI, Header, HTTPException, Response, status
app = FastAPI()
_store: dict[int, dict] = {42: {'id': 42, 'name': 'Pippa', 'version': 17}}
def compute_etag(resource: dict) -> str:
"""싼 ETag: JSON 직렬화 state 의 hash."""
body = json.dumps(resource, sort_keys=True).encode()
return f'"{hashlib.sha256(body).hexdigest()[:12]}"'
@app.get('/users/{uid}')
async def read_user(uid: int, response: Response,
if_none_match: str | None = Header(None, alias='If-None-Match')):
user = _store[uid]
etag = compute_etag(user)
response.headers['ETag'] = etag
response.headers['Vary'] = 'Accept-Encoding'
if if_none_match == etag:
# Client cache 최신 — body 없는 304 돌려줌
response.status_code = status.HTTP_304_NOT_MODIFIED
return None
return user
@app.put('/users/{uid}')
async def update_user(uid: int, payload: dict, response: Response,
if_match: str | None = Header(None, alias='If-Match')):
user = _store[uid]
current_etag = compute_etag(user)
if if_match and if_match != current_etag:
# Stale ETag — 거부
raise HTTPException(status.HTTP_412_PRECONDITION_FAILED,
detail='resource modified; re-fetch 후 재시도')
# Update 적용
_store[uid] = {**user, **payload, 'version': user['version'] + 1}
response.headers['ETag'] = compute_etag(_store[uid])
return _store[uid]
이전 lesson 의 FastAPI server 를 완전한 conditional 지원으로 확장: 모든 GET 에 ETag 발신, If-None-Match 일치 시 304 돌려줌, PUT/PATCH/DELETE 에 If-Match 불일치 시 412 돌려줌. 그 다음 loop 도는 Python client 써: GET → ETag 저장 → PUT-with-If-Match. 같은 resource 에 동시 loop 돌리는 client 둘 spawn 해서 싸우는 거 봐 — 하나는 성공 (200), 하나는 짐 (412), 설계 그대로. 지는 client 는 re-fetch 후 재시도 해야.
Hint
ETag 계산: JSON 직렬화 resource 의 SHA256 (deterministic 위해 sort_keys=True). 같은 v17 읽는 두 client 가 같은 ETag 계산. 첫 PUT 성공해서 version v18 로 올림; 두 번째 PUT 의 If-Match 가 stale 되어 412 받음. 지는 client 의 회복 loop: 412 잡음 → re-GET → 변경 재적용 → 새 ETag 로 re-PUT. 이 패턴이 모든 collaborative editor 가 조용한 덮어쓰기 피하는 방식.
Progress
Progress is local-only — sign in to sync across devices.