Cupix::Errors::BadGateway: {"result"=>{"code"=>"BG10004", "type"=>"Cupix::Errors::BadGateway", "reason"=>"Failed to fetc
RCA: Cupix::Errors::BadGateway BG10004 — Failed to fetch facility info (401 Unauthorized)
Overview#
What Happened#
Facility migration 요청 처리 중 migration-service(cupixworks monorepo)가 tesla API(GET /api/v1/facilities/{id})를 호출해 source/destination facility 정보를 가져오려다 tesla 가 401 Unauthorized 를 반환했다. migration-service 는 이 401 을 Cupix::Errors::BadGateway(code BG10004)로 감싸 502 로 응답하고 error 레벨 로그를 남긴다. 2025-11-28 부터 2026-07-28 까지 약 8개월간 51건이 산발적으로 발생했다.
APM service 필드는 cupixworks-api 로 기록되어 있으나, 에러 메시지("Failed to fetch facility info from tesla server")와 code BG10004 문자열은 tesla 가 아니라 tesla 를 호출하는 migration-service 에서 발생한 것이다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Cupix::Errors::BadGateway |
| exception.message | Unable to retrieve facility info: 401 Unauthorized |
| top_frame | applications/migration-service/app/factories/migration_factory.rb:174-180 |
| runtime | Ruby (Rails, cupixworks migration-service) |
| env | production (cross-environment facility migration flow) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| migration-service (facility migration) | 51 (8개월 누적) | facility migration 요청이 502 로 실패. 만료/무효 토큰으로 재시도한 소수 사용자에 국한 |
Timeline#
- 2025-11-28 14:45 KST — 최초 발생 (first_seen, representative sample)
- 2026-07-28 20:08 KST — 최근 발생 (last_seen)
- 2026-08-04 — RCA 수행
Error Log#
{"result"=>{"code"=>"BG10004", "type"=>"Cupix::Errors::BadGateway", "reason"=>"Failed to fetch facility info from tesla server", "message"=>"Unable to retrieve facility info: 401 Unauthorized"}}
Impact#
- Service:
cupixworks-api(APM 태그; 실제 발생 서비스는migration-service) - 발생 횟수: 51
- 최초 발생: 2025-11-28 14:45 KST
- 최근 발생: 2026-07-28 20:08 KST
Root Cause Summary#
facility migration 검증 흐름에서 MigrationFactory.set_facility_team_ids 가 caller 로부터 전달받은 from[:auth_token] / to[:auth_token] 로 source/destination tesla endpoint 의 GET /api/v1/facilities/{id} 를 호출한다. 이 토큰이 해당 endpoint(환경/리전/tenant)에 대해 만료되었거나 유효하지 않으면 tesla 가 401 Unauthorized 를 반환한다. fetch_facility_info 의 rescue RestClient::Exception 절은 모든 upstream 실패를 401(client 인증 오류)/500/503(server 오류) 구분 없이 일괄로 Cupix::Errors::BadGateway(BG10004)로 감싸 raise 하고, 이는 server_error_controller.rb 에서 502 응답 + Cupix::Logger.error 로 기록되어 Error Tracking 에 잡힌다. 즉 client-side 인증 실패(401)를 server-side gateway 오류(502 error)로 오분류하는 것이 근본 원인이다.
Technical Analysis#
Code Path#
- Entry point:
applications/migration-service/app/controllers/migrations_controller.rb:48(MigrationsController#create) - caller 가
from/to페이로드(각auth_token포함)를 전달
def create
migration = MigrationFactory.create(
session: params[:session],
type: params[:type],
source: params[:source],
from: params[:from],
to: params[:to],
sender: params[:sender],
service_name: params[:service_name]
)
- facility & non-move migration 일 때만 tesla 조회 진입:
migration_factory.rb:28-31
if to[:model_name] == 'facility' && type != 'move'
set_facility_team_ids(from, to)
validate_facility_settings_match(from, to) if production_destination?(to)
end
- source/destination endpoint 를 각각 조회하며 caller 토큰 사용:
migration_factory.rb:43-51
def set_facility_team_ids(from, to)
from_endpoint = MigrationOperation.generate_tesla_endpoint(from[:environment], from[:region], from[:tenant])
to_endpoint = MigrationOperation.generate_tesla_endpoint(to[:environment], to[:region], to[:tenant])
from[:facility_info] = fetch_facility_info(from_endpoint, from[:model_id], from[:auth_token])
to[:facility_info] = fetch_facility_info(to_endpoint, to[:model_id], to[:auth_token])
- Failure point:
migration_factory.rb:174-180— tesla 401 을 잡아 무조건 BG10004(502) 로 변환
response = RestClient.get(url, { 'x-cupix-auth': auth_token })
facility_data = JSON.parse(response.body)
# ...
rescue RestClient::Exception => e
Cupix::Logger.error("Failed to fetch facility info from #{url} - #{e.response.body}", class: self.class.name, function: __method__, error: e)
raise Cupix::Errors::BadGateway.new(
code: 'BG10004',
reason: 'Failed to fetch facility info from tesla server',
message: "Unable to retrieve facility info: #{e.message}"
)
- 변환된 예외는 502 + error 로그로 처리:
server_error_controller.rb:7-19
rescue_from Cupix::Errors::BadGateway, with: :sever_502_error
def sever_502_error(exception)
raise_error(
502,
exception,
code: 'BG10000',
type: Cupix::Errors::BadGateway, reason: 'BadGateway',
message: exception.message
)
Cupix::Logger.error("[Migration] Server 502 error - #{exception}", class: self.class.name, function: __method__)
end
기대 동작 vs 실제 동작: 기대 — 유효한 토큰이라면 facility 정보를 200 으로 받아 migration 진행. 실제 — 토큰이 대상 endpoint 에 유효하지 않으면 tesla 가 401 을 반환하고, migration-service 는 이를 502 BadGateway 로 오분류하여 error 로그를 남긴다. 401(요청자 인증 문제)과 500/503(tesla 서버 문제)이 동일 경로로 뭉개져 알람 노이즈가 된다.
Log Evidence#
Datadog 로그 검색 (retention 14일 내):
service:cupixworks-api "Failed to fetch facility info" → 0 logs
service:migration-service "Failed to fetch facility info" → 0 logs
"Failed to fetch facility info from" → 0 logs
Representative sample(Error Tracking first_seen, 2025-11-28)의 원문:
{"result":{"code":"BG10004","type":"Cupix::Errors::BadGateway","reason":"Failed to fetch facility info from tesla server","message":"Unable to retrieve facility info: 401 Unauthorized"}}
et: Error Tracking issue 특성상 last_seen(2026-07-28) 이 Datadog 로그 retention(14일) 밖이라 매칭 로그 0건이다. 다만 이 issue 의 message 포맷은 코드에 상수로 고정(Unable to retrieve facility info: #{e.message})되어 있고, e.message 가 401 Unauthorized 인 것은 tesla 401 응답을 그대로 담은 것이므로, representative 와 current occurrence 모두 동일한 401 시나리오임이 코드로 확정된다 — Representative Error 와 현재 발생 메시지 간 불일치는 관찰되지 않는다.
repo 확정 증거 (grep):
/repos/cupixworks/applications/migration-service/app/factories/migration_factory.rb:178
reason: 'Failed to fetch facility info from tesla server'
/repos/cupixworks/applications/migration-service/app/factories/migration_factory.rb:179
message: "Unable to retrieve facility info: #{e.message}"
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | caller 가 대상 endpoint 에 유효하지 않은/만료된 auth_token 을 전달 → tesla 401 → migration-service 가 502 BadGateway 로 오분류 | migration_factory.rb:47-48 caller 토큰 사용; :174-180 모든 RestClient::Exception 을 BG10004 로 변환; message 에 401 Unauthorized 그대로 노출 |
— | Confirmed |
| H2 | tesla 서버 장애(500/503)로 facility 조회 실패 | 동일 rescue 경로가 500/503 도 BG10004 로 감쌈 | representative/observed message 가 401 Unauthorized 로 명시 — 서버 5xx 가 아님 |
Rejected |
| H3 | 외부 의존성 outage 인시던트의 일부 | status-board svc:cupixworks-api::unknown, 이 cluster id 포함 active 인시던트 없음 |
2026-07-30 resolved 인시던트는 다른 cluster id 집합이며 last_seen(07-28)과도 무관 | Rejected |
| H4 | 발생 서비스가 tesla(cupixworks-api) 자체 | APM service 태그 cupixworks-api |
에러 문자열/코드가 tesla repo 에 없고 migration-service 에만 존재 (grep) | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 조치 불필요 수준의 즉시 코드 변경 없음. 401 은 요청자 토큰 문제이므로 서버 결함이 아니다.
단기 개선 (1주 이내)#
migration_factory.rb:174-180의rescue RestClient::Exception을 상태 코드별로 분기:RestClient::Unauthorized(401)/RestClient::Forbidden(403) 같은 client 인증 오류는Cupix::Logger.warn+ 4xx 계열 client 오류(예:Cupix::Errors::Parameter또는 인증 전용 코드)로 매핑하고, 500/503 등 tesla 서버 오류만BG10004502 +error로 유지.fetch_levels(:190-197),fetch_floorplans(:199~),fetch_access_code/fetch_access_token등 동일 패턴의 sibling 메서드도 함께 정렬.- 목적: 요청자 인증 실패(사용자 조치 대상)와 tesla gateway 장애(운영 알람 대상)를 구분해 Error Tracking 알람 노이즈 제거.
장기 개선 (재발 방지)#
- migration 시작 전
from/to토큰의 유효성/스코프를 사전 검증(preflight)하여, cross-environment/region migration 에서 토큰이 대상 endpoint 에 유효한지 먼저 확인 후 사용자에게 명확한 4xx 메시지 반환.
Monitoring#
- migration-service 의 facility 조회 401 발생 추이(warn 재분류 후):
sum:trace.rack.request.errors{service:migration-service,http.status_code:401}.as_count()
- migration-service 전체 502 응답 추이(tesla gateway 실장애 신호):
sum:trace.rack.request.errors{service:migration-service,http.status_code:502}.as_count()
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard (rescue 분기 좁힘 + sibling 메서드 정렬 + spec 업데이트)
Noise Verdict#
noise — 401 은 요청자가 전달한 만료/무효 토큰에 의한 client 인증 실패로, tesla 나 migration-service 의 코드 결함이 아니며 8개월간 51건으로 산발적이다(다만 401→502 error 오분류로 인한 알람 노이즈는 rescue 분기 좁힘으로 개선 권장).