본문으로 건너뛰기
개발

외부 API 403의 원인이 된 User-Agent 버전 표기

외부 API 403의 원인이 된 User-Agent 버전 표기

개인 프로젝트로 운영하는 게임 정보 사이트 홈에는 쿠폰과 공지가 나온다. 게임 공식 포럼의 공개 API에서 글을 읽어 와서, 15분마다 다시 갱신(재검증)하는 구조다. 재검증은 Vercel의 운영 서버 함수(요청을 처리하는 서버리스 함수)가 맡는다.

어느 날 공식 포럼에 올라온 새 쿠폰 2개가 홈 쿠폰 목록에 없는 것을 발견했다.

저장본 대비책에 가려진 실패

홈은 에러 화면 없이 정상으로 보였다. 포럼 요청이 실패하면 미리 저장해 둔 저장본(snapshot)을 보여 주게 만들어 뒀기 때문이다.

이 대비책 덕분에 사용자는 에러 화면을 보지 않았지만, 같은 이유로 나는 실패를 알아채지 못했다. 운영 로그를 보니 포럼 요청이 계속 403(접근 거부)으로 실패하고 있었다. 그동안 홈은 오래된 저장본을 보여 주고 있었다.

원인은 User-Agent의 버전 표기

같은 요청을 로컬에서 직접 보내 봤다.

User-Agent의 Chrome 버전 표기결과
Chrome/130.0403
Chrome/130.0.0.0200

실제 Chrome의 User-Agent는 버전을 점으로 구분한 숫자 네 개(130.0.0.0)로 쓴다. 코드에는 숫자 두 개로 적혀 있었다. 포럼 앞단의 필터가 형식이 맞지 않는 User-Agent를 막기 시작한 것으로 보인다. 필터가 언제부터, 정확히 어떤 규칙으로 바뀌었는지는 알 수 없다.

같은 값이 세 파일(운영 서버 함수 하나, 저장본 생성 스크립트 둘)에 흩어져 있었다. 이번에는 세 곳을 모두 고치고 주석을 달았다. 하나로 모으는 일은 후속 과제로 남겼다.

TS
// Chrome 버전은 숫자 네 개(130.0.0.0)여야 한다 (Chrome/130.0 → 403)
const USER_AGENT = "... Chrome/130.0.0.0 ...";

이 필터가 브라우저가 아닌 호출을 막으려는 의도인지는 확인하지 못했다. 공개 API라도 상대 서비스의 이용 규칙은 지켜야 하므로, 포럼 이용 약관을 다시 확인하고 호출 주기는 15분으로 유지하기로 했다.

운영 서버 함수에서 다시 확인

로컬에서 200이 나오고, 저장본을 다시 받으니 쿠폰 9개가 들어왔다. 테스트와 빌드도 통과했다.

다만 그때는 운영 서버 함수에서도 통과하는지 아직 확인하지 못한 상태였다. 로컬과 빌드 머신은 운영 서버 함수와 IP가 달라서, 필터가 IP까지 보고 있다면 결과가 다를 수 있다.

그래서 배포 후 두 시점을 나눠서 봤다.

확인 시점데이터를 읽은 곳쿠폰 수저장본 안내 문구
배포 직후빌드 머신9개없음
재검증 후운영 서버 함수9개없음

배포 직후 화면은 빌드 머신이 읽은 데이터다. 운영 서버 함수가 직접 읽은 데이터는 15분 재검증이 지나야 나온다. 그래서 재검증 시각까지 기다렸다가 두 번째 줄을 확인했다.

에러 로그도 다시 봤다. 마지막 403은 새 배포가 만들어지기 전 시각이었고, 그 뒤로 확인한 시점까지는 0건이었다.

실패 감지 개선 계획

이번 일에서 고칠 점은 403을 늦게 알았다는 것이다. 저장본으로 대체하는 설계는 유지하되, 실패가 계속되면 내가 알 수 있어야 한다.

저장본 안내 문구가 화면에 나오는지 주기적으로 확인하고, 같은 외부 API 실패가 일정 횟수 이상 이어지면 알림을 받도록 할 계획이다. 둘 다 아직 붙이지는 않았다.

정리

  • 대비책(저장본)은 사용자를 보호하지만, 실패를 숨기기도 한다. 대비책이 동작하는 걸 운영자가 알 수 있어야 한다.
  • 외부 API의 403은 내 코드를 바꾸지 않아도 상대 필터가 바뀌어 생길 수 있다. 요청 헤더를 실제 브라우저와 하나씩 대조해 본다.
  • 같은 상수가 여러 파일에 흩어져 있으면 같은 버그가 여러 번 생긴다.
  • "고쳐졌다"는 증거는 운영 환경에서 확인해야 한다. 빌드 머신에서 성공해도 운영 서버에서는 결과가 다를 수 있다.