CORS — 명시 목록이지 와일드카드가 아니야
cwkPippa 의 CORS 는 명시적인 허용 목록이고, 생각보다 길어. 로컬 개발 포트, 클라이언트를 돌리는 맥마다의 tailnet 주소, 그리고 브라우저에서 직접 PIN 세션을 받는 형제 앱마다의 서빙 origin — 서른 개쯤 — 에다, 브라우저 확장이랑 데스크톱 webview origin 을 위한 패턴 하나. 새 클라이언트? 그 origin 을 일부러 추가해. 패턴도 목록만큼 조심해야 해. 허용 목록이 몰래 넓어지는 자리가 바로 거기거든. Starlette 는 패턴을 origin 전체에 맞춰 보니까, 호스트 뒤에 .* 가 붙으면 그 호스트로 시작하기만 하는 도메인은 다 통과해. 그리고 CORS 에러가 쌓일 때 다들 손 뻗는 지름길 allow_origins=['*'] 는 사적인 박스에서도 보안 구멍이야. CORSMiddleware 자체는 닫힌 채로 시작해. origin 을 하나도 안 적으면 아무것도 허용 안 해.
CORS 가 어디 앉느냐도 뭘 허용하느냐만큼 중요해. 일부러 가장 바깥 middleware 로 뒀어. PIN 검사가 요청을 거부할 때 그 401 에도 CORS 헤더가 붙어야 하거든. 안 그러면 브라우저가 응답 본문을 CORS 에러 뒤에 숨기고, 클라이언트는 'PIN 이 틀림' 이랑 '서버가 죽음' 을 구분 못 해.
Middleware — 순서가 곧 설계인 스택
평범한 요청은 일곱 층을 지나. 바깥부터: CORS, PIN 인증, 누가 묻는지를 요청 범위에 묶는 사용자 해석기, admin override 를 기록하는 감사 층, /api JSON 용 gzip. gzip 은 SSE 스트림을 절대 안 건드리게 지어졌어. 그 안쪽엔 conditional GET 이 있어. gzip 안에 있어야 ETag 가 압축 전 본문 기준으로 잡히고, 답을 이미 들고 있는 폰은 빈 304 만 받아(2026-10-10 추가, mobile 트랙 레슨 8). 맨 안쪽은 private door 야. 비공개 대화를 가리키는 경로는 거기 적힌 route 에만 닿아(2026-09-26). 층이 하나 더 있어. CORS 보다 나중에 등록돼서 엄밀히는 나머지를 다 감싸는 family gateway(2026-09-22)야. gateway 경로 하나만 처리하고 다른 요청은 그대로 넘겨. chat 과 cron 사이의 mutex 는 middleware 가 아니야 — route 가 대화마다 직접 불러 (Heart track, lesson 4). middleware 는 가볍고 범용으로 둬. 비싸거나 특정한 건 route 나 백그라운드 task 몫이야.
에러 — 시끄럽게, 정직하게 실패해
조용한 catch 는 없어. adapter 가 raise 하면 route 가 SSE 로 error event 를 띄워서 프론트엔드가 'I got stuck — try again' 을 보여주게 해. chat 이 '괜찮아 보이게' 예외를 삼키지 마. 다른 각도에서 본 broken-chat 죄야.
로그 — 기록이 아니라 운영자를 위한 것
서버 로그는 타임스탬프가 붙은 평범한 줄이고, 관련된 vessel 이나 계정 태그가 달려 있어. grep 하기 쉽고, JSON 은 아니야. 일부러 나눈 역할이야. 대화의 영속 기록은 로그가 아니라 JSONL 이거든 (Truth track). 로그는 운영자가 실패를 재구성하라고 있는 거고, JSONL 은 아빠가 본 게 하나도 안 사라지게 하려고 있는 거야.
production 은 '내 맥에선 되네' 다음에 시작해. 허용한 origin 이 실제로 서빙되는 URL 과 맞는지, SSE 에러가 HTTP 200 안에 숨지 않고 화면의 상태로 바뀌는지 확인해. 그럴듯해 보이는 설정이랑, 실제 요청이 그 경로를 지나가는 건 서로 다른 주장이야.
로그에도 경계가 있어. 편하다고 prompt 원문이나 자격증명을 통째로 남기면, 관측 가능성을 얻는 대신 다른 사고를 만들어. 이벤트 종류, 걸린 시간, provider, 결과처럼 진단에 필요한 모양만 남기고 민감한 payload 는 요약하거나 빼. 좋은 로그는 모든 걸 적는 장부가 아니라, 실패를 재구성할 만큼만 정직한 장부야.
그리고 middleware 에 정책을 모으고 싶은 마음을 참아. 모든 route 를 지나가니까 business rule 까지 넣고 싶어지는데, 그러면 숨은 조상이 돼. 모든 요청에 진짜로 공통인 일만 두고, 도메인 판단은 이름 있는 service 랑 route 에서 보여줘.