[Integration] failed to refresh token for bim360 integration(2332) - state: failed, error_message: B
RCA: [Integration] failed to refresh token for bim360 integration(2332)
Overview#
What Happened#
2026-07-13 13:07 KST 에 cupixworks-migration-worker 의 Cupix::Cron::Integration.renew_before_expiration 스케줄러가 BIM360 통합(integration_id=2332) 의 OAuth access token 을 만료 직전에 갱신하려 시도했고, Autodesk Forge 의 refreshtoken 엔드포인트가 400 Bad Request 를 반환하면서 실패했다. 응답 본문에는 developerMessage 가 비어 있어 로그에는 원인이 빠진 채 "BIM360 Authentication failed:" 로만 기록되었고, 통합 레코드는 failed 상태로 전이되었다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Cupix::Errors::Parameter (code ARG10000) |
| exception.message | BIM360 Authentication failed: (developerMessage 비어 있음) |
| top_frame | app/repositories/integration_repository.rb:140 |
| upstream error | RestClient::BadRequest — 400 Bad Request from Autodesk Forge refreshtoken |
| env | production, us-west-2 |
| tenant | cupix |
| affected_integration | bim360 integration(2332) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| Integrations (BIM360 OAuth) | 1 (본 클러스터) | integration_id=2332 이 failed 상태로 전이되어 재인증 전까지 해당 테넌트의 BIM360 자산 접근 불가 |
| Integrations (BIM360 OAuth, 서비스 전반) | 최근 24h 동안 10건 이상 (Datadog service:cupixworks-migration-worker "BIM360 refresh_token failed") |
여러 integration id 에서 동일 400 응답 반복 발생. 각 테넌트별 재인증 필요 |
Timeline#
- 2026-06-30 14:18 KST — integration(2332) 의 access_token 이 발급됨 (
expired_at: 2026-06-30 05:18:14 UTC→ 재갱신으로 갱신되는 필드; 로그에 남은 refresh 직전 값) - 2026-07-13 13:07:15 KST —
renew_before_expirationcron 이 후보로 integration(2332) 를 선택,refresh_token시작 (state: active) - 2026-07-13 13:07:19 KST — Autodesk Forge
refreshtoken이400 Bad Request로 응답,developerMessage필드 비어 있음 - 2026-07-13 13:07:19 KST —
Bim360Operation.refresh_token에서Cupix::Errors::Parameter로 wrap,IntegrationRepository#refresh_token이failed_state!호출, error 로그 기록 - 2026-07-13 13:07:19 KST — cron 상위
Cupix::Cron::Integration.renew_before_expiration이 rescue 한 뒤 다음 후보로 진행
Error Log#
[Integration] failed to refresh token for bim360 integration(2332) - state: failed, error_message: BIM360 Authentication failed:
Impact#
- Service:
cupixworks-migration-worker - 발생 횟수: 1 (본 클러스터). 유사 fingerprint 를 가진 다른 integration_id 실패까지 합하면 24h 내 10건 이상
- 최초 발생: 2026-07-13 13:07 KST
- 최근 발생: 2026-07-13 13:07 KST
- 상태 영향: integration(2332) 가
failed로 전이되어, 이후IntegrationRepository#refresh_token및access_token진입 시check_failed_state에서 즉시 실패 (재인증 UI 로 유저 개입 필요)
Root Cause Summary#
Autodesk Forge OAuth refreshtoken 엔드포인트가 integration(2332) 의 저장된 refresh_token 에 대해 HTTP 400 Bad Request 를 반환했고, 응답 JSON 의 developerMessage 필드가 비어 있어 원인이 로그로 전달되지 못했다. Autodesk 측에서 refresh_token 이 무효화된 (사용자 revoke, 앱 secret 재발급, 3-legged 인증 재수행 필요 등) 것이 실제 원인이지만, Bim360Operation.refresh_token 의 RestClient::Exception rescue 가 developerMessage 만 로그 및 Cupix::Errors::Parameter#reason 에 심어 넣기 때문에 로그 상 원인이 소실되었다. IntegrationRepository#refresh_token 은 StandardError 를 rescue 하면서 refresh_token_response_body = token 을 대입하는데, 이 시점에 token 은 아직 정의되지 않아 실제로는 nil 로 저장된다 — 응답 본문 원본이 DB 에도 남지 않아 사후 분석이 더 어려워진다.
Technical Analysis#
Code Path#
- Entry point:
lib/cupix/cron/integration.rb:9—IntegrationRepository.new(model).refresh_token - Provider dispatch:
app/repositories/integration_repository.rb:107-121—case @model.provider when 'bim360' then Bim360Operation - HTTP call:
app/operations/bim360_operation.rb:62—Cupix::HttpClient.post(url, data, header)(Autodeskrefreshtoken) - Failure point (Autodesk 400):
app/operations/bim360_operation.rb:63-70—RestClient::Exceptionrescue,Cupix::Errors::Parameter로 wrap - Log emit / state 전이:
app/repositories/integration_repository.rb:130-142
begin
url = "#{$OAUTH[:autodesk_forge][:site]}#{$OAUTH[:autodesk_forge][:refresh_url]}"
response = Cupix::HttpClient.post(url, data, header)
rescue RestClient::Exception => e
response = JSON.parse(e.response)
Cupix::Logger.error("BIM360 refresh_token failed: #{response['developerMessage']} - error: #{e.message}")
raise Cupix::Errors::Parameter.new(
code: 'ARG10000',
reason: "BIM360 Authentication failed: #{response['developerMessage']}"
)
기대 동작: Autodesk 가 refresh 실패 시 developerMessage 로 원인을 반환 → 로그와 exception 에 명시. 실제 동작: 이번 사례에서 developerMessage 가 비어 있어 "BIM360 Authentication failed:" 로만 남고, raise 시 message: 도 넘기지 않아 BaseError#initialize 가 message = reason 으로 대체 — 이후 e.message 조회도 원인 정보 없음.
begin
unless check_refresh_request
raise Cupix::Errors::Parameter.new(code: 'ARG10060', reason: 'Refresh access token request is too many')
end
token = operation_class.refresh_token(@model.refresh_token, @model.region)
uncheck_refresh_request
rescue StandardError => e
uncheck_refresh_request
raise e if e.respond_to?(:code) && e.code == 'ARG10060'
@model.refresh_token_failed_at = DateTime.now
@model.refresh_token_expired_at = nil
@model.refresh_token_response_body = token
@model.failed_state!
Cupix::Logger.error("[Integration] failed to refresh token for #{@model.provider} integration(#{@model.id}) - state: #{@model.state}, error_message: #{e.message}")
raise e
end
기대 동작: 실패 원본 응답을 refresh_token_response_body 에 저장하여 사후 재현 가능. 실제 동작: token local 변수가 rescue 진입 시점에 정의되지 않았으므로 (호출부 operation_class.refresh_token 이 예외를 던져 대입이 이뤄지지 않음) nil 이 저장된다. check_refresh_request 로 잡히는 rate-limit 경로(ARG10060) 에서는 이 대입 자체가 도달 전에 re-raise 되지만, 그 외 실패에서는 원본 응답이 유실되는 것과 동일.
def initialize(**opts)
super
if opts[:code].blank?
Airbrake.notify('code is missing') do |notice|
notice[:params]
end
end
self.code = opts[:code]
self.type = self.class.name
self.status_code = opts[:status_code]
self.reason = opts[:reason]
self.message = opts[:message] || opts[:reason]
end
Bim360Operation.refresh_token 의 raise 는 message: 를 넘기지 않으므로 .message = reason = "BIM360 Authentication failed:" 가 된다. 이 값이 그대로 IntegrationRepository#refresh_token 의 rescue 로 흘러가 error_message: 로 노출된다.
Log Evidence#
Datadog 쿼리:
service:cupixworks-migration-worker status:error "BIM360 Authentication failed"
integration(2332) 앞뒤 로그 (service:cupixworks-migration-worker "integration(2332)", 최근 7일):
2026-07-13 13:07:19 info [Integration] refresh token for bim360 integration(2332) - state: active, expired_at: 2026-06-30 05:18:14 UTC, refresh_token_expired_at: 2026-07-14 04:07:15 UTC
2026-07-13 13:07:19 error [Integration] failed to refresh token for bim360 integration(2332) - state: failed, error_message: BIM360 Authentication failed:
refresh_token_expired_at 이 2026-07-14 04:07:15 UTC 로 아직 유효했음에도 Autodesk 가 400 을 반환 — 애플리케이션의 만료 시계와 별개로 Autodesk 측에서 이미 refresh_token 을 무효화했다는 강한 정황.
동일 fingerprint 의 사전 로그 (service:cupixworks-migration-worker "BIM360 refresh_token failed", 24h):
2026-07-13 13:07:19 BIM360 refresh_token failed: - error: 400 Bad Request
2026-07-13 09:07:34 BIM360 refresh_token failed: - error: 400 Bad Request
2026-07-13 09:07:34 BIM360 refresh_token failed: - error: 400 Bad Request
2026-07-13 01:07:29 BIM360 refresh_token failed: - error: 400 Bad Request
2026-07-13 01:07:29 BIM360 refresh_token failed: - error: 400 Bad Request
2026-07-13 01:07:29 BIM360 refresh_token failed: - error: 400 Bad Request
2026-07-13 01:07:27 BIM360 refresh_token failed: - error: 400 Bad Request
2026-07-13 01:07:27 BIM360 refresh_token failed: - error: 400 Bad Request
2026-07-12 21:07:41 BIM360 refresh_token failed: - error: 400 Bad Request
2026-07-12 21:07:41 BIM360 refresh_token failed: - error: 400 Bad Request
developerMessage 자리가 항상 비어 있음 ("failed: - error:" 이중 공백) → Autodesk 응답 JSON 에 필드가 없거나 빈 문자열. cron 스케줄 (config/schedule.rb:94: every '7 */4 * * *', 매 4시간 07분 실행) 시각과 로그 시각이 일치 (01:07, 05:07/09:07, 13:07, 21:07 KST) → cron 이 만료 임박 후보를 골라 실패가 일괄 발생.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Autodesk 가 refresh_token 을 revoke/rotate 하여 400 반환 (실제 root cause). 로컬 refresh_token_expired_at 은 아직 유효하지만 remote 상태와 불일치 | refresh_token_expired_at: 2026-07-14 04:07:15 UTC 여전히 유효, 그럼에도 400. state: active → failed 전이. cron 시각마다 동일 400 반복 |
— | Confirmed |
| H2 | Cupix 애플리케이션 buglet — 잘못된 request body/헤더로 400 을 계속 유발 | 동일 코드 경로가 다른 통합에 대해 정상 동작하는 로그 존재 (Successfully refreshed token 는 별도 통합에 대해 관측). data/header 는 $OAUTH 설정 + refresh_token 값만 사용 |
다른 integration 은 성공하는데 특정 integration 만 400 → 코드 문제라기보다 자원 상태 문제 | Rejected |
| H3 | Autodesk Forge 전역 장애 | 다른 integration 은 성공 (Datadog Successfully refreshed token 로그 관측), 4시간 간격 cron 마다 특정 integration 만 실패 |
전역 장애면 모든 400 이 아니라 500/503 이 나거나 전 통합이 실패해야 함 | Rejected |
| H4 | refresh_token_response_body = token 라인의 로컬 변수 미정의 (token 이 rescue 시점에 nil) 가 root cause |
코드 상 명백한 문제 (line 137) | 이는 사후 진단을 어렵게 만드는 secondary bug 일 뿐, 400 자체를 유발하지 않음 | Rejected (as root cause), Confirmed as secondary bug |
| H5 | 로그의 빈 원인 문자열은 developerMessage 필드 자체가 응답에 없어서 발생 |
모든 실패 로그가 "failed: - error:" 이중 공백 (fmt string #{response['developerMessage']} 에 nil/blank interpolation) |
— | Confirmed (secondary — 로그 품질 결함) |
Fix Recommendation#
즉시 조치 (Critical)#
- integration(2332) 의 소유 테넌트에게 재인증 안내: BIM360 (Autodesk Forge) OAuth 재승인 필요. 앱 (Warden/Frontend) 의 재인증 흐름으로 유도하며, DB 에서
refresh_token_failed_at이 세팅된 통합을 CSV 로 뽑아 운영에 전달. - 최근 24h 내 400 을 받은 다른 integration id 도 함께 취합 (
service:cupixworks-migration-worker "BIM360 refresh_token failed"→ 로그의integration(id)파싱). 위 로그에서 관찰된 id: 2332, 1160, 1163, 1428, 1234, 1405, 1473, 1482, 1472, 1470 등. - 코드 변경은 필수 아님 — 재인증은 유저/테넌트 액션으로 해결.
단기 개선 (1주 이내)#
app/operations/bim360_operation.rb:66,69,72,75의 로그/reason 문자열에 Autodesk 응답 본문 전체(또는error/errorCode필드) 를 함께 담아 원인 유실 방지.developerMessage만 사용하지 말고 fallback 으로response['error'] || response['errorCode'] || e.response.to_s[0..200]를 포함.app/repositories/integration_repository.rb:137—token변수는 rescue 시점에 미정의될 수 있으므로refresh_token_response_body대입은 실패 응답 본문을 별도로 캡처해 저장 (예:Bim360Operation에서 raise 시e.response.body를 exception attribute 로 노출).Cupix::Cron::Integration.renew_before_expiration(lib/cupix/cron/integration.rb:11) 의 실패 로그에integration(id)및 provider 를 함께 남겨 상위 rescue 에서도 대상 식별 가능하도록 (현재는e.message만 로그).
장기 개선 (재발 방지)#
- BIM360 통합의
failed전이가 감지되면 자동으로 담당 테넌트/유저에게 재인증 안내 알림 (email/in-app) 을 보내는 파이프라인 도입. 현재는 상태만 세팅되고 사용자가 자산 접근을 시도할 때까지 인지되지 않음. - 통합별 마지막 성공/실패 시각과 원인 카테고리 (
invalid_grant,rate_limited,network등) 를 별도 카운터/게이지 메트릭으로 노출해 외부 IdP 이슈를 대시보드에서 확인. - Autodesk 의 3-legged OAuth 흐름 특성상 refresh_token 이 사용자 액션(권한 취소, 비밀번호 변경 등) 으로 무효화되는 것을 정상 시나리오로 취급 —
state: failed로 전이하는 것은 유지하되, 이 케이스는 error 가 아니라warn레벨로 다운그레이드 하는 것을 검토 (현재는 매 cron 실행마다 error 로그 다수 발생).
Monitoring#
Datadog 대시보드에 추가할 timeseries widget:
- BIM360 refresh 실패 빈도 (분당 카운트)
service:cupixworks-migration-worker status:error "BIM360 refresh_token failed"
- Integration 상태 전이 실패 (provider 별)
service:cupixworks-migration-worker status:error "[Integration] failed to refresh token"
- 응답 원인 유실 여부 (double-space marker) — 이중 공백은
developerMessage가 비어있음을 뜻함
service:cupixworks-migration-worker "BIM360 refresh_token failed: - error:"
- Cron 상위 rescue 카운트
service:cupixworks-migration-worker status:error "[Cupix::Cron::Integration] failed to renew integration"
알람 후보: 위 첫 번째 쿼리가 4시간 창(cron 주기) 에 5건 이상이면 warn, 20건 이상이면 alert. 스파이크 유형(전 통합 vs 특정 통합) 을 구분하기 위해 아래 쿼리도 별도 구성.
Risk Assessment#
- Risk level: low (외부 IdP 상태 문제로 해당 테넌트에 국한, cron 상위에서 rescue 되어 다른 통합 갱신은 정상 진행)
- 예상 복잡도: trivial (즉시 조치는 재인증 안내로 해결; 단기 개선은 로그/응답 캡처 강화 수준)