🌐 httpbin.org 503 / CORS 콘솔 에러 — 네트워크 품질 체크 원인 분석

마지막 업데이트 2026-07-22

게스트(/client-guest) 콘솔에 10초마다 HEAD https://httpbin.org/status/200 net::ERR_FAILED 503 + CORS policy: No 'Access-Control-Allow-Origin' header 에러가 반복적으로 찍히는 케이스. 분석 결과 원인은 외부 무료 서비스(httpbin.org)의 503 다운이며, 리소스 다운로드 · AI 세션 · 미디어 서버 어디에도 영향이 없는 콘솔 노이즈로 확인됨. 최종 결정(2026-06-05 갱신): C안 채택 — "사용자 인터넷 전체 다운 vs 우리 서버 다운"을 구분하는 진단 가치를 살리기 위해, 불안정한 httpbin.org 를 독립적이고 신뢰성 높은 연결성 엔드포인트(예: gstatic.com/generate_204)로 교체하고, 현재 죽어 있는 determineIssue/issue 신호를 실제 소비처에 연결한다.

1. 기본 정보

증상게스트 콘솔에 10초 주기로 httpbin.org/status/200 503 + CORS 차단 에러 반복
발생 위치apps/web/shared/lib/use-network-quality.tsmeasureExternalLatency()
호출 주기setInterval(checkQuality, 10000) — 게스트 페이지 동안 10초마다
요청fetchWithAlert("https://httpbin.org/status/200", { method: "HEAD", mode: "cors", cache: "no-store" })
실측 근거LogRocket 세션(019e922b…) — httpbin 503 구간 중에도 S3 리소스 25개 전부 정상 캐싱
관련 시스템V2 (FSD) 게스트 페이지 — 네트워크 품질 표시 위젯

2. 핵심 결론

근본 원인 외부 무료 테스트 서비스 httpbin.org 의 503 Service Unavailable. httpbin.org 는 공개 무료 서비스라 과부하·다운이 잦고, 현재 503 을 반환 중. 우리 코드 버그가 아님.

CORS 는 503 의 부수 증상 httpbin 이 503 에러를 낼 때 그 에러 응답에는 Access-Control-Allow-Origin 헤더가 없다. 그래서 mode: "cors" 요청의 응답 읽기가 브라우저에 의해 차단 → CORS 에러 + ERR_FAILED 까지 같이 찍힘. 즉 CORS 에러는 별개 문제가 아니라 503 응답의 헤더 부재에서 파생된 것.

서비스 영향 없음 리소스 다운로드 · AI 세션 · 미디어 서버 어디에도 영향 없음. 호출은 try/catch 로 감싸져 실패해도 throw 하지 않고 externalOk=false 로만 처리됨. 게다가 이 외부 체크 결과는 코드 어디서도 읽지 않는 죽은 데이터(아래 4·5장).

유일한 실질 피해 콘솔 오염(10초마다 에러 스택) 과, 외부 연결이 정상이어도 항상 비정상으로 판정되는 issue 필드 오판 — 그런데 그 issue 를 읽는 코드도 없어 사용자 영향 0.

3. 원인 메커니즘 — 왜 503, 왜 CORS

3-1. 네트워크 품질 체크의 의도

useNetworkQuality"사용자 네트워크 문제"인지 "우리 서버 문제"인지 구분하기 위해 두 곳을 동시에 측정한다:

// apps/web/shared/lib/use-network-quality.ts
async function measureExternalLatency() {            // 외부 인터넷 도달성
  await fetchWithAlert("https://httpbin.org/status/200",
    { cache: "no-store", method: "HEAD", mode: "cors" });
}
async function measureServerLatency() {              // 우리 서버 도달성
  await fetchWithAlert("/api/health",
    { cache: "no-store", method: "HEAD" });
}
// determineIssue(externalOk, serverOk):
//   external O / server X → "server"        (우리 서버 문제)
//   external X / server X → "user-network"  (사용자 망 문제)
//   external O / server O → "none"

3-2. 두 에러가 겹치는 과정

  1. 503 — httpbin.org 가 과부하로 Service Unavailable 응답.
  2. CORS 차단 — 503 응답에 CORS 허용 헤더가 없어, mode:"cors" 요청의 응답을 브라우저가 차단.
  3. ERR_FAILED — 위 차단으로 fetch Promise reject → 콘솔 스택 출력.
  4. catch 로 흡수use-network-quality.tstry/catch 가 잡아 externalLatency=null, externalOk=false 로만 처리. 앱 흐름 영향 없음.
참고: /api/health(서버 체크)는 동일 origin이라 CORS 무관하며 정상 동작. 문제는 외부 체크의 httpbin 의존 하나뿐.

4. 서비스 영향 분석 — 코드 + 로그 교차 검증

대상영향근거
리소스(S3 영상) 다운로드 영향 없음 use-resource-cache.ts/api/resources/urls + fetchResumable(presignedUrl) 경로는 httpbin 과 완전 별개. CORS 는 요청별·origin별 격리라 전파 안 됨.
AI 세션 (OpenAI Realtime) 영향 없음 별도 WebRTC/토큰 경로. 세션 로그상 AI audio producer 정상 생성.
미디어 서버 (mediasoup SFU) 영향 없음 별도 소켓/transport 경로. 세션 로그상 transport connected 유지, producer 정상.
네트워크 품질 UI 경미 quality/latency 표시는 /api/health 기반이라 정상. httpbin 결과(issue)만 오판되나 읽는 곳 없음.

4-1. CORS 전파 불가 — 왜 S3 다운로드를 못 막는가

CORS 차단은 해당 요청 하나에만 적용된다. httpbin.org 로의 fetch 가 막혀도 다른 origin(S3)으로의 fetch 에는 영향이 없다. "한 origin 이 막혔으니 다른 것도 막는다" 같은 전파(cascade)나 연결 풀 공유로 인한 차단은 존재하지 않는다.

httpbin.org503 응답 + CORS 헤더 없음 → 브라우저가 응답 읽기 차단 → 콘솔 에러
S3 presigned URL200 응답 + 정상 CORS 헤더(버킷 설정) → 정상 다운로드

4-2. 로그 실증 — 503 구간에도 S3 25개 전부 성공

httpbin 이 503 을 반복하던 동일 시간대에 S3 영상/이미지 리소스가 전부 정상 캐싱됨:

[RESOURCE_CACHE] Successfully cached video_firstopeningJW_yoona.mp4
[RESOURCE_CACHE] Successfully cached video_pingpong01_bridge_jw01~04.mp4
... (총 25개 전부 성공, 실패 0건)
[GUEST_PAGE_SESSION] Resources cached successfully
httpbin 503/CORS 와 S3 영상 다운로드는 인과관계가 없다. 세션 로그상 ERROR 0건, WARN 은 STT 중복 청크 스킵(정상 dedup)뿐.

5. 데이터 흐름 — httpbin 결과는 "죽은 데이터"

httpbin 호출이 만들어내는 값이 어디서 소비되는지 전수 추적한 결과:

필드출처소비처
quality/api/health (serverLatency)UI 표시
latency/api/health (serverLatency)UI 표시 + 네트워크 알림
externalLatencyhttpbin아무도 안 읽음
issuehttpbin + health 조합아무도 안 읽음

소비처 3곳(app/client-guest/page.tsx, widgets/guest/guest-layout/ui/guest-layout.tsx, components/pages/guest-entry.tsx)은 모두 .quality.latency 만 사용하며, 이 둘은 전부 /api/health 기반이다.

httpbin 이 결정하는 externalLatency/issue계산만 되고 어디서도 읽히지 않는다. 따라서 외부 체크를 통째로 제거해도 기능 손실이 0이다.

6. 수정 — C안: 독립 엔드포인트로 교체 + issue 신호 활성화

결정 변경 이력: 최초(2026-06-04)에는 "issue가 미사용이니 외부 체크를 제거(A안)"로 정리했으나, "사용자 인터넷 전체가 죽었는가 vs 우리 서버 문제인가"를 실제로 구분하고 싶다는 요구(C안)가 확정되어 제거 → 교체 + 신호 활성화로 방향을 변경한다.

6-1. 왜 소켓 서버(/health)로는 안 되는가

소켓 서버(socket-prod./health)는 여전히 우리 인프라다. 웹과 다른 호스트이긴 하나 동일 리전/ALB권이라, 사용자 인터넷이 끊기면 웹·소켓이 같이 실패해 "사용자 망 다운"을 가릴 수 없다. 게다가 /health는 Redis가 degraded면 503을 내므로(apps/socket/src/sfu-api/restHandlers.ts:441) 503 노이즈가 형태만 바뀐다. → C안의 기준점으로 부적합.

6-2. 채택 — 독립적·고신뢰 연결성 엔드포인트로 교체

채택 외부 체크 대상만 httpbin → 연결성 체크 전용 제3자 엔드포인트로 교체한다. 본문을 읽지 않고 도달성·지연만 재므로 mode: "no-cors"(opaque)로 호출 → CORS 차단 없음. 503이 사실상 없는 대형 인프라라 노이즈도 제거된다.

async function measureExternalLatency() {
  const start = performance.now();
  // no-cors: 응답 본문은 못 읽지만 "도달 성공/실패 + 시간"은 확보, CORS 차단 없음
  await fetchWithAlert("https://www.gstatic.com/generate_204", {
    cache: "no-store",
    method: "GET",       // 204 엔드포인트는 GET 권장 (HEAD 미지원 가능)
    mode: "no-cors",
    timeoutMs: 3000,     // 100% 패킷 손실 시 무한 대기 방지
    skipSlowAlert: true,
  });
  return Math.round(performance.now() - start);
}

6-3. issue 신호 활성화 (죽은 데이터 → 실사용)

외부+서버 조합으로 determineIssue를 살려 원인을 가르고, 그 issue를 UI/알림 등 실제 소비처에 연결한다(현재는 계산만 되고 미사용 — 5장).

external (제3자)server (/api/health)판정
OKOKnone — 정상
OK실패server — 우리 서버 문제
실패실패user-network — 사용자 인터넷 전체 다운
실패OKunknown — 제3자 일시 장애 등 (드묾)

6-4. 함께 처리

검토했으나 미채택

7. 한 줄 요약

콘솔의 httpbin.org 503/CORS 에러는 외부 무료 서비스 다운이 원인이며, try/catch 로 흡수되는 콘솔 노이즈일 뿐 리소스 다운로드·AI 세션·미디어 서버에 영향이 전혀 없다. CORS 는 요청별로 격리돼 S3 다운로드로 전파되지 않고, 503 구간에도 S3 리소스 25개가 정상 캐싱됐다. 최종 방향(C안): "사용자 인터넷 전체 다운 vs 우리 서버 다운" 구분 가치를 살리기 위해, 불안정한 httpbin 을 gstatic.com/generate_204 같은 독립·고신뢰 엔드포인트로 no-cors 교체하고(503/CORS 노이즈 제거), 죽어 있던 determineIssue/issue 신호를 실제 소비처에 연결한다. 소켓 /health는 우리 인프라라 부적합.

관련 문서: 아동274 아동 3회 접속 실패 분석 · 리소스 다운로드 HTTP Range 이어받기 · 아동004 29회기 수업 중단 — 단말 인터넷 단절 (NETWORK_QUALITY 실증)

분석 작성: 2026-06-04 / 결정 갱신(C안): 2026-06-05 / chulsu