"REST 가 request/response. Webhook 이 그거 뒤집음 — server 가 너의 endpoint 의 client 됨. 202 + Location 이 REST 의 '이거 시간 걸려; 여기 update 위해 poll' 말 방식. 둘 다 HTTP 안 떠나고 동기 request timeout 탈출 가능."
Webhook — 뒤집힌 호출
Webhook 이 그냥 관심 event 일어나면 service 가 고객 제공한 URL 에 만드는 HTTP POST. Stripe 가 payment 성공 시 네 endpoint ping; GitHub 가 누가 pull request 열면 네 거 ping; SendGrid 가 email bounce 시 네 거 ping.
Shape: 고객이 너한테 URL 등록; event 일어나면 그 URL 에 JSON body POST. 고객 endpoint 가 성공에 2xx 돌려줌. 나머지 다 네 문제.
Webhook 보내기 어려운 세 가지
1. 신뢰성. 고객 server 가 다운, 느림, 잘못될 수도. Sender 로서 네 일: exponential backoff 로 retry (예: 즉시, 1m, 5m, 30m, 1h, 6h, 24h 까지 3 일), 그 다음 dead-letter (log + alert). Stripe 가 3일 retry; GitHub 가 ~8시간.
2. 검증. 고객 endpoint 가 네 IP 에서 unsolicited POST 받음. 진짜 너고 attacker pretending 아닌 거 어떻게 알아? 공유 secret 으로 HMAC-SHA256 으로 각 payload 서명; header 에 signature 포함. 고객이 검증. Stripe 의 Stripe-Signature, GitHub 의 X-Hub-Signature-256.
3. Idempotency. Retry 의미가 같은 event 2-7 번 전달 가능. 각 전달이 receiver 가 dedupe 가능한 unique event_id 가짐. Receiver 일이 handler idempotent 만들기 (혹은 event_id 써서 중복 건너뜀).
Receiver 가 해야 하는 것
빨리 2xx 응답. Webhook sender 가 초 내 response 기대; 느린 response 가 timeout 과 retry 야기. Payload 받고, 작업 enqueue, 즉시 200 돌려줌.
Signature 검증. 공유 secret 으로 raw body 의 HMAC 계산; header signature 와 비교. 불일치면 401 거부.
event_id 로 dedupe. 본 ID 저장 (TTL 가진 Redis 동작). 중복 보면 재처리 없이 200 돌려줌.
공개. Endpoint 가 인터넷에서 도달 가능 필수. 로컬 개발이 localhost 노출 위해 ngrok 같은 거 필요.
Async-202 패턴 — Webhook 없는 REST
때로 고객 URL 없음; 그냥 느린 operation 있음. 202 Accepted 가 REST 의 답:
Client 가 work POST. Server 가 enqueue, 즉시 Location: /jobs/abc 가진 202 Accepted 돌려줌.
Client 가 status 가 done (혹은 failed) 까지 GET /jobs/abc poll.
Job resource 가 status, progress, (완료 시) 결과나 결과 link 운반.
이게 Stripe 가 payout 처리, AWS 가 long batch operation 처리, Cloudflare 가 bulk DNS update 처리 방식. Client 가 절대 장기 HTTP 연결 보유 대기 안 함; 싼 status endpoint poll.
Webhook 과 202+Location 이 '이 작업이 한 request/response 에 안 맞음' 위한 두 REST 패턴. Webhook = 뭔가 일어나면 server 가 너한테 push. 202 = server 가 너한테 status URL poll 하라 알려줌. 둘 다 WebSocket 이나 영속 연결 없이 HTTP 안 async work 살게.
cwkPippa 의 async 현실
cwkPippa 가 council finalization 에 202 + Location 씀 — 4 brain council 이 30-90초 걸림, 어느 합리적 HTTP timeout 도 한참 넘음. Finalize POST 가 job URL 가진 202 돌려줌; frontend 가 job 이 finalized round link 가진 status: done 운반할 때까지 2s 마다 poll. cwkPippa 가 현재 webhook 안 보냄 (제 3자 소비자 없음), 근데 upstream provider 에서 RECEIVES — Anthropic 과 OpenAI 둘 다 청구 event 위해 cwkPippa 의 webhook receiver ping. Receiver 가 signature 검증, event_id 로 dedupe, 작업 enqueue, 200 빨리 돌려줌. 표준 패턴.
Code
Webhook receiver: HMAC 검증, id 로 dedupe, enqueue, 2xx 빨리 돌려줌·python
# Server 쪽 — HMAC signature 검증 가진 webhook 받기
import hashlib, hmac
from fastapi import FastAPI, Request, Header, HTTPException, status
app = FastAPI()
WEBHOOK_SECRET = b'env-에서-온-공유-secret'
_seen_event_ids: set[str] = set() # production 엔 TTL 가진 Redis
@app.post('/webhooks/stripe')
async def stripe_webhook(
request: Request,
stripe_signature: str = Header(None, alias='Stripe-Signature'),
):
raw_body = await request.body()
# 1. Signature 검증
expected = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(stripe_signature.split('=')[-1], expected):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail='잘못된 signature')
payload = await request.json()
event_id = payload.get('id')
# 2. event_id 로 dedupe (retry 가 같은 id 전달)
if event_id in _seen_event_ids:
return {'received': True, 'duplicate': True}
_seen_event_ids.add(event_id)
# 3. 작업 enqueue; 2xx 빨리 돌려줌
enqueue_payment_processed(payload['data']['object'])
return {'received': True}
Async 202 패턴: enqueue, Location 돌려줌, status endpoint 노출·python
FastAPI 에 두 가지 만들어: (1) POST /webhooks/test 에 공유 secret 에 대해 HMAC-SHA256 signature 검증하고, payload 에서 event_id 로 dedupe 하고, 200 빨리 돌려주는 webhook receiver; (2) POST /jobs 가 202 + Location + job_id 돌려주고, GET /jobs/{id} 가 status: queued / running / done 보고하는 async job endpoint. (1) 테스트: 같은 event_id 로 같은 webhook 두 번 보내 — 두 번째가 'duplicate' 돌려줘야. (2) 테스트: job 시작하고 완료까지 poll.
Hint
HMAC signature: hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest(). hmac.compare_digest (constant-time) 로 검증. Dedupe 엔 in-memory set 이 연습에 괜찮; production 은 TTL 가진 Redis. Async job 이 BackgroundTasks 씀; status endpoint 가 그냥 dict lookup. 2s sleep 으로 status URL polling 이 canonical client 패턴 — 절대 장기 HTTP 연결 보유 대기 안 하는 거 봐.
Progress
Progress is local-only — sign in to sync across devices.