ES /docs

[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-workerCupix::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::BadRequest400 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#

  1. 2026-06-30 14:18 KST — integration(2332) 의 access_token 이 발급됨 (expired_at: 2026-06-30 05:18:14 UTC → 재갱신으로 갱신되는 필드; 로그에 남은 refresh 직전 값)
  2. 2026-07-13 13:07:15 KSTrenew_before_expiration cron 이 후보로 integration(2332) 를 선택, refresh_token 시작 (state: active)
  3. 2026-07-13 13:07:19 KST — Autodesk Forge refreshtoken400 Bad Request 로 응답, developerMessage 필드 비어 있음
  4. 2026-07-13 13:07:19 KSTBim360Operation.refresh_token 에서 Cupix::Errors::Parameter 로 wrap, IntegrationRepository#refresh_tokenfailed_state! 호출, error 로그 기록
  5. 2026-07-13 13:07:19 KST — cron 상위 Cupix::Cron::Integration.renew_before_expiration 이 rescue 한 뒤 다음 후보로 진행

Error Log#

Datadog Logs

text
[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_tokenaccess_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_tokenRestClient::Exception rescue 가 developerMessage 만 로그 및 Cupix::Errors::Parameter#reason 에 심어 넣기 때문에 로그 상 원인이 소실되었다. IntegrationRepository#refresh_tokenStandardError 를 rescue 하면서 refresh_token_response_body = token 을 대입하는데, 이 시점에 token 은 아직 정의되지 않아 실제로는 nil 로 저장된다 — 응답 본문 원본이 DB 에도 남지 않아 사후 분석이 더 어려워진다.

Technical Analysis#

Code Path#

  • Entry point: lib/cupix/cron/integration.rb:9IntegrationRepository.new(model).refresh_token
  • Provider dispatch: app/repositories/integration_repository.rb:107-121case @model.provider when 'bim360' then Bim360Operation
  • HTTP call: app/operations/bim360_operation.rb:62Cupix::HttpClient.post(url, data, header) (Autodesk refreshtoken)
  • Failure point (Autodesk 400): app/operations/bim360_operation.rb:63-70RestClient::Exception rescue, Cupix::Errors::Parameter 로 wrap
  • Log emit / state 전이: app/repositories/integration_repository.rb:130-142
app/operations/bim360_operation.rb:60-70ruby
    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:" 로만 남고, raisemessage: 도 넘기지 않아 BaseError#initializemessage = reason 으로 대체 — 이후 e.message 조회도 원인 정보 없음.

app/repositories/integration_repository.rb:123-143ruby
    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 되지만, 그 외 실패에서는 원본 응답이 유실되는 것과 동일.

lib/cupix/errors/base_error.rb:10-24ruby
      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 쿼리:

text
service:cupixworks-migration-worker status:error "BIM360 Authentication failed"

integration(2332) 앞뒤 로그 (service:cupixworks-migration-worker "integration(2332)", 최근 7일):

text
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):

text
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:137token 변수는 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 실패 빈도 (분당 카운트)
text
service:cupixworks-migration-worker status:error "BIM360 refresh_token failed"
  • Integration 상태 전이 실패 (provider 별)
text
service:cupixworks-migration-worker status:error "[Integration] failed to refresh token"
  • 응답 원인 유실 여부 (double-space marker) — 이중 공백은 developerMessage 가 비어있음을 뜻함
text
service:cupixworks-migration-worker "BIM360 refresh_token failed:  - error:"
  • Cron 상위 rescue 카운트
text
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 (즉시 조치는 재인증 안내로 해결; 단기 개선은 로그/응답 캡처 강화 수준)