
오타/잘못된 내용에 대한 지적은 언제나 환영입니다!
안녕하시렵니까.
EC2, Docker를 활용한 배포 이전에 간단히 외부 서버에서의 동작이 원활하게 작동하는지 테스트 해보고자 ngrok을 활용했습니다.
그러나! CORS 문제가 발생했는데..
ngrok을 이용해 로컬 서버를 외부에 노출할 때, SockJS 기반의 WebSocket 연결에서 CORS 정책과 관련된 문제와 해결 방안에 대해 포스팅하고자 합니다.
특히, ngrok 무료 버전에서 발생하는 경고 페이지 때문에 SockJS의 초기 연결 요청인 "/connect/info?t=..."에 대해 JSON이 아닌 HTML 응답이 반환되어 정상적인 WebSocket 핸드셰이크가 실패하는 문제를 해결하는 과정을 다룹니다.
ngrok을 사용해 프론트엔드와 백엔드를 연결(ws)하는 과정에서 CORS 정책 위반 문제가 발생함.
특히 SockJS의 초기 연결 요청(/connect/info?t=timestamp)에서 예상치 못한 응답이 돌아오면서 오류가 발생했음.
ngrok 무료 버전은 보안 및 사용 정책상, 최초 연결 시 브라우저 경고 페이지를 출력함.
그러나 SockJS는 초기 요청 시 커스텀 헤더를 포함할 수 없으므로, 경고 페이지를 건너뛸 방법이 제한적임.
결과적으로, 기대했던 JSON 응답 대신 text/html 응답이 반환되면서 문제가 발생함.

ngrok에 배포하고 나니까 WebSocket CONNECT(handshake) 가 실패합니다.

Access to XMLHttpRequest at 'https://.../connect/info?t=174016xxxxx' from origin 'https://...'
has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*'
when the request's credentials mode is 'include'.
- 'Access-Control-Allow-Origin' header in the response must not be the wildcard ''.
→ 서버의 응답 헤더에서 Access-Control-Allow-Origin: (모든 도메인 허용)으로 설정되어 있음.- when the request's credentials mode is 'include'.
→ 클라이언트가 credentials: "include" 옵션을 사용해 쿠키, 인증 정보 등을 포함한 요청을 보냈음. (SockJS 연결시 "include" 옵션이 설정됨)- 🚨 문제 발생:
CORS 정책상, credentials: "include"를 사용하면 Access-Control-Allow-Origin 값을 *로 설정할 수 없음. 반드시 특정 도메인(예: https://example.com)을 지정해야 함.
로그를 살펴보니, CORS 문제,,
네트워크 통신 로그를 살펴볼까?


Request Headers 부분을 살펴보면 "/connect/info?t=~~" 경로로 요청은 정상적인 것을 알 수 있습니다.
그러나, Response Headers 부분을 살펴보면 Access-Control-Allow-Origin에 "*" (모든 요청 허용)이 되어있습니다.
하지만, 서버측에서 CORS 경로가 설정되어 있으며 서버 로그에서나 네트워크 통신 로그에서 init 호출은 200(OK) 정상 response 된 것을 보면 서버측은 문제가 없었습니다.



문제가 없는데 도대체 왜????????
클라이언트는 서버와의 handShake 이전, 연결 방식을 확인하기 위해 요청을 보냅니다.

응답에는 서버에서 지원하는 전송 방식이 포함됩니다.
여기서 "websocket": true라면 WebSocket을 바로 사용할 수 있으며, "websocket": false라면 XHR Streaming이나 Long Polling 같은 대체 방법을 사용하게 됩니다.

근데 Response Headers의 Content-Length와 Type이 수상합니다.. 왠 text/html???
기대하는 응답은 분명 application/json Type인데 뭐지?

그래서, 웹 브라우저에서 해당 URL로 직접 접속해봤습니다.
이런! ngrok 무료버전은 이런 경고성? 홍보성? 페이지를 거치기 때문에 기대와는 다른 전혀 이상한 html 응답이 도착한 것이었습니다.
이런 페이지를 skip하는 방법이 있는데, 아래와 같이 요청 header에 'ngrok-skip-browser-warning': anyText 를 포함시키면 됩니다.

그런데.. SockJS의 Connect는 Header를 포함시킬 수 없는 요청입니다.

SockJS는 브라우저 환경에서 최대한 호환성을 유지하려고 합니다.
SockJS의 초기 /connect/info 요청은 WebSocket의 정식 핸드셰이크 단계 전에 어떤 전송 방식이 지원되는지 확인하는 요청이기 때문이다. 이 단계에서 커스텀 헤더를 보내는 건 브라우저에서 허용되지 않는다
WebSocket 연결 이후(connect/websocket 등)에는 헤더를 넣을 수 있음.
stompClient.connect(headers, onConnect, onError);
이 단계에서는 SockJS가 WebSocket 또는 XHR 연결을 결정한 이후라서 가능.
즉, SockJS의 "/connect" 요청시에 필연적으로 저 페이지를 만나야 하며 그렇게 되면 정상적인 response를 받을 수 없게됩니다. (저 html이 자꾸 response됨 하..)
그렇다면 방법이 없을까요?

임의로 해당 "/connect/info?t=timestamp" URL 을 방문해서 Visite Site 버튼을 누르게 되면, 쿠키에 상태를 저장하게됩니다. 이후의 WS CONNECT 요청에는 해당 페이지를 Skip 하며, 정상적으로 handShake의 결과를 response 합니다.

ngrok 관련 레퍼런스가 너무 없어서 진짜 화병날 번 했지만, 짜잔~ 정상적으로 해결

예외적으로, 크롬 시크릿 모드에서는 Cookie 사용을 허용하지 않기 때문에 계속 저 페이지를 Skip할 수 없습니다.
이상 ngrok 사용시에 SockJS connect 요청시 발생하는 CORS 문제에 대해 다루어보았습니다.