버그 #601
진행중[버그] edge.alphea.dev 응답의 Access-Control-Allow-Origin 중복으로 Hub 웹의 모든 Connect RPC 가 브라우저에서 차단됨 (구글 로그인 불가)
0%
설명
edge.alphea.dev 가 실제 응답에 Access-Control-Allow-Origin 헤더를 두 번 내려보내 브라우저가 응답을 차단합니다. 그 결과 hub.alphea.dev 에서 모든 Connect RPC 호출이 실패하며, 구글 로그인이 콜백 단계에서 끝나지 않습니다. 게이트웨이 측 확인을 요청드립니다.
현상¶
구글 계정 선택까지는 정상 진행되고 https://hub.alphea.dev/auth/google/callback?state=... 로 정상 복귀하지만, 화면이 "Finishing Google sign-in" 에서 멈추고 아래 메시지가 표시됩니다.
Connect could not be reached. Try again shortly.
재현 환경¶
- staging (
https://hub.alphea.dev→https://edge.alphea.dev) - 확인 일시: 2026-08-11
- 브라우저: Chrome (데스크톱)
- Hub Web 커밋: 695784f (main)
브라우저 콘솔 에러 로그¶
Access to fetch at 'https://edge.alphea.dev/alphea.connect.v1.AuthService/GoogleLogin' from origin 'https://hub.alphea.dev' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values 'https://hub.alphea.dev, https://hub.alphea.dev', but only one is allowed. Have the server send the header with a valid value. edge.alphea.dev/alphea.connect.v1.AuthService/GoogleLogin:1 Failed to load resource: net::ERR_FAILED
값은 동일하지만 헤더가 두 개 존재하며, CORS 규격상 Access-Control-Allow-Origin 은 정확히 하나여야 하므로 브라우저가 응답 전체를 차단합니다.
확인한 내용 — 실제 응답 헤더가 중복입니다¶
POST /alphea.connect.v1.AuthService/GoogleLogin · Origin: https://hub.alphea.dev
$ curl -s -D - -o /dev/null -X POST \
"https://edge.alphea.dev/alphea.connect.v1.AuthService/GoogleLogin" \
-H "Origin: https://hub.alphea.dev" \
-H "Content-Type: application/json" -d '{}' \
| grep -i "access-control\|vary" | sort | uniq -c
2 Access-Control-Allow-Origin: https://hub.alphea.dev ← 중복
2 Access-Control-Expose-Headers: Grpc-Status, Grpc-Message,
Grpc-Status-Details-Bin,
Connect-Protocol-Version ← 중복
2 Vary: Origin ← 중복
RequestEmailChallenge 도 동일하게 중복됩니다. 즉 구글 로그인만의 문제가 아니라 이메일 OTP 를 포함한 모든 Connect RPC 가 브라우저에서 똑같이 차단됩니다.
preflight 는 정상이라 더 위험합니다¶
OPTIONS 는 헤더가 하나뿐이라 통과합니다.
$ curl -s -D - -o /dev/null -X OPTIONS \
"https://edge.alphea.dev/alphea.connect.v1.AuthService/GoogleLogin" \
-H "Origin: https://hub.alphea.dev" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type" \
| grep -i "access-control\|vary" | sort | uniq -c
1 Access-Control-Allow-Headers: content-type
1 Access-Control-Allow-Methods: POST
1 Access-Control-Allow-Origin: https://hub.alphea.dev ← 여기는 정상 1개
1 Access-Control-Max-Age: 86400
1 Vary: Origin, Access-Control-Request-Method, ...
그래서 다음 순서로 진행됩니다.
- preflight 통과 → 브라우저가 실제 POST 를 전송
- 서버는 요청을 정상 처리 (이 시점에 구글 인가 코드가 소모됨)
- 응답은 돌아오지만 브라우저가 ACAO 중복으로 차단 →
fetch가 reject - SDK 가 이를
unavailable로 매핑 → 화면에 "Connect could not be reached"
인가 코드는 이미 소모된 뒤라 재시도해도 동일하게 실패합니다.
원인 위치 — 두 계층이 각각 CORS 를 붙이고 있습니다¶
Origin 을 바꿔 가며 어느 계층이 헤더를 붙이는지 분리했습니다.
Origin: https://hub.alphea.dev → ACAO 2개, Vary: Origin 2개 Origin: https://example.com → ACAO 0개, Vary: Origin 2개 ← 양쪽 모두 거부 Origin 헤더 없음 → ACAO 0개, Vary: Origin 2개
Vary: Origin 이 어떤 경우에도 항상 2개라는 점이 결정적입니다. 두 계층이 모두 CORS 로직을 실행하고 있고, 양쪽 다 동일한 origin 허용목록을 각자 들고 각자 헤더를 붙이는 중복 구성입니다.
-
앞단 openresty (응답 헤더
Server: openresty,X-Served-By: edge.alphea.dev) — 설정 위치를 특정하지 못했습니다. Network/인프라 소유로 보입니다. -
업스트림 alphea-rpc (
alphea-edge-core/alphea-rpc/internal/server/server.go:94,rs/cors) — 응답의Access-Control-Expose-Headers값이 이 코드의ExposedHeaders목록과 정확히 일치하므로 중복 헤더 중 하나는 여기서 나옵니다.
corsMiddleware := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"*"},
ExposedHeaders: []string{"Grpc-Status", "Grpc-Message",
"Grpc-Status-Details-Bin", "Connect-Protocol-Version"},
AllowCredentials: false,
MaxAge: 86400,
})
다만 코드상 AllowedOrigins 는 "*" 인데 실제 배포본은 example.com 을 거부했으므로, 운영 중인 인스턴스는 환경 설정의 origin 허용목록으로 구동 중입니다. alphea-edge-core 레포 내 hub.alphea.dev 문자열 검색은 0건이었습니다.
제안 — 한쪽만 남기면 됩니다¶
A안 (권장) alphea-rpc 를 CORS 단일 출처로 두고, openresty 에서 CORS add_header 를 제거합니다. origin 허용목록 로직이 애플리케이션에 이미 있고, Grpc-* expose 헤더처럼 프로토콜을 아는 값은 프록시가 아니라 서버가 관리하는 편이 맞습니다.
B안 openresty 를 단일 출처로 두어야 한다면, 프록시에서 업스트림 헤더를 숨깁니다.
proxy_hide_header Access-Control-Allow-Origin; proxy_hide_header Access-Control-Expose-Headers; proxy_hide_header Vary;
함께 요청드립니다 — X-Alphea-Request-Id 노출¶
Access-Control-Expose-Headers 에 X-Alphea-Request-Id 가 빠져 있습니다. 이 때문에 SDK 가 trace id 를 읽지 못하고 오류 화면에도 표시되지 않아, 장애 발생 시 서버 로그와 대조할 수단이 없습니다. 이번 수정 시 함께 추가해 주시면 좋겠습니다.
수정 후 검증 방법¶
$ curl -s -D - -o /dev/null -X POST \
"https://edge.alphea.dev/alphea.connect.v1.AuthService/GoogleLogin" \
-H "Origin: https://hub.alphea.dev" \
-H "Content-Type: application/json" -d '{}' \
| grep -ci "^access-control-allow-origin"
1 ← 1 이 나와야 정상 (현재는 2)
참고 — 정상 확인된 항목¶
Hub Web 측에는 수정할 부분이 없음을 함께 확인했습니다.
| 항목 | 상태 |
|---|---|
배포 번들의 VITE_CONNECT_BASE_URL
|
정상 (https://edge.alphea.dev) |
| 배포 번들의 Google client id | 정상 |
| Connect 엔드포인트 도달성 | 정상 (400 invalid_argument 정상 응답) |
| preflight CORS | 정상 |
| 실제 응답 CORS 헤더 | 중복 — 본 이슈 |
HP Henry Park이(가) 3달 전에 변경
- 상태을(를) 신규에서 진행(으)로 변경되었습니다.
원인 확정 및 수정 진행 상황입니다. 원인 계층이 이슈 본문과 다릅니다 — 아래 정정을 참고해 주세요.
재현 확인¶
보고된 절차 그대로 독립 재현했습니다. 실제 POST 응답에 Access-Control-Allow-Origin 2개, Access-Control-Expose-Headers 2개, Vary: Origin 2개. preflight(OPTIONS)는 각각 1개로 정상 통과. 보고 내용과 일치합니다.
정정 — 중복을 만드는 두 계층¶
이슈 본문은 앞단 openresty + 업스트림 alphea-rpc로 보았으나, 양쪽 팀이 배포본을 각각 읽어 같은 결론에 도달했습니다. 실제로는 둘 다 애플리케이션 계층입니다.
- 공개 edge gateway의 CORS 핸들러
- 상위 Foundation 루트 핸들러의 CORS
Network(앞단)에는 CORS를 주입하는 설정이 없습니다. 인프라 담당이 무변경 상태로 독립 확인했습니다 — 프록시 설정 트리에 add_header 등 관련 지시어 0건, edge 호스트는 순수 TLS 리버스 프록시. openresty는 이미 중복된 응답을 앞에서 전달할 뿐입니다.
따라서 제안 B안의 proxy_hide_header는 이 결함에 적용되지 않습니다. 앞단은 애초에 헤더를 붙이지 않으며, 애플리케이션이 소유자인 상태에서 프록시가 상위 헤더를 숨기면 소유자의 헤더가 가려집니다.
확정된 수정 방향 — A안과 같은 취지¶
애플리케이션이 CORS 단일 소유자가 되고, 내보내기 전에 상위 계층의 CORS 헤더를 제거합니다. 프록시는 건드리지 않습니다.
두 팀이 각각 만든 작업 중 하나를 취소하여 단일 실행 경로로 정리했습니다. Network 쪽은 브랜치/설정/재시작/배포 모두 없이 무변경으로 종료되었습니다.
진행 상태¶
수정 병합 완료, 파이프라인 통과. 현재 staging 배포 진행 중입니다.
완료 판정 기준¶
배포 성공만으로 해결로 보지 않습니다. 아래를 모두 확인한 뒤에만 해결 처리합니다.
- 실제 POST 응답의
Access-Control-Allow-Origin1개 - preflight 정상
- 허용되지 않은 origin은 여전히 거부
-
Origin헤더가 없는 요청도 정상 - 사람이 실제로 구글 로그인을 끝까지 완료
5번이 핵심입니다. preflight가 통과하는 구조 때문에 서버는 요청을 처리하고 인가 코드를 소모한 뒤 응답만 차단됩니다. 헤더만 보고 판단하면 사용자 관점의 실패를 놓칠 수 있습니다.
함께 요청된 X-Alphea-Request-Id 노출¶
이번 수정에서 분리합니다. 별도 후속으로 다룹니다.
분리 사유: CORS가 차단하면 브라우저가 응답 자체를 넘기지 않으므로, 이 결함에 한해서는 헤더를 노출해도 SDK가 trace id를 읽을 수 없습니다. 다만 응답이 정상 도달하는 다른 오류에서는 여전히 필요하므로, 그 근거로 별도 진행합니다.
※ 본 코멘트는 Heze가 작성하여 Henry Park 계정으로 등록되었습니다.