ES /docs

BIM360 refresh_token failed: - error: 400 Bad Request

RCA: BIM360 refresh_token failed: - error: 400 Bad Request

Overview#

What Happened#

2026-07-13 13:07 KST 에 cupixworks-migration-worker 의 4시간 주기 통합 갱신 크론(Cupix::Cron::Integration.renew_before_expiration)이 BIM360(Autodesk Forge) refresh_token 만료 예정인 integration 을 순회하며 Autodesk 토큰 엔드포인트를 호출했다. 그중 일부 integration 에 대한 응답이 HTTP 400 Bad Request 로 반환되었고, Bim360Operation.refresh_token 이 응답 본문에서 developerMessage 필드를 찾지 못한 채 Cupix::Logger.error 로 이를 기록해 error 로그로 노출되었다. 동일 크론 실행에서 다른 BIM360 integration 은 정상적으로 갱신되었으므로 전역 장애가 아니라 개별 계정 단위 문제이다.

Quick Facts#

Field Value
exception.class Cupix::Errors::Parameter (code ARG10000)
exception.message BIM360 refresh_token failed: - error: 400 Bad Request
top_frame app/operations/bim360_operation.rb:66
upstream Autodesk Forge OAuth token endpoint ($OAUTH[:autodesk_forge][:refresh_url])
env production, region us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
Integrations / BIM360 1 (이 클러스터), 관련 클러스터 3건 동일 시점 발생 해당 integration(state: failed) 을 사용하는 프로젝트에서 BIM360 자산 접근 실패. Autodesk 재인증 필요

status-board 상 동일 서비스에서 같은 시점(2026-07-13 13:07 KST) 4개 cluster 가 열린 인시던트(2026-07-13-svc-cupixworks-migration-worker--unknown-1) 로 묶여 있다.

Timeline#

  1. 2026-07-13 09:07 KST — 이전 크론 사이클. integration(1160), integration(1163) 에 대해 동일한 BIM360 refresh_token failed: - error: 400 Bad Request 발생, integration state → failed 전이.
  2. 2026-07-13 13:07:19 KST — 이 클러스터의 대표 로그 발생. integration(2332) (직전 상태 active) 에 대해 refresh 시도, Autodesk 가 400 Bad Request 응답. state → failed. 동일 배치의 integration(7295/7298/7299) 는 성공.
  3. 감지 — error-sweeper collector 가 fingerprint bf5327d789818de62530896fd1ed1ee2 로 클러스터 파일 생성.

Error Log#

Datadog Logs

text
BIM360 refresh_token failed:  - error: 400 Bad Request

Impact#

  • Service: cupixworks-migration-worker
  • 발생 횟수: 1 (이 fingerprint), 동일 시점 관련 클러스터 3건 (status-board incident 2026-07-13-svc-cupixworks-migration-worker--unknown-1)
  • 최초 발생: 2026-07-13 13:07 KST
  • 최근 발생: 2026-07-13 13:07 KST

Root Cause Summary#

정기 refresh 크론이 BIM360 integration 의 refresh_token 을 Autodesk Forge 에 제출했지만, Autodesk 가 HTTP 400 Bad Request 로 거부했다. Bim360Operation.refresh_token 은 400 응답의 JSON 본문에서 developerMessage 필드를 그대로 문자열 보간하는데, Autodesk 의 OAuth 표준 오류 응답에는 developerMessage 대신 error/error_description 필드가 있다. 그 결과 로그 메시지는 원인 문자열이 비어 있는 "BIM360 refresh_token failed: - error: 400 Bad Request" 형태로 남았다. 400 자체의 근본 원인은 개별 integration 의 refresh_token 이 무효화(사용자가 Autodesk 측에서 앱 접근을 취소했거나, 이전에 갱신되지 않아 만료됨)된 것이며, 시스템 전역 장애는 아니다. 실제로 같은 크론 실행에서 다른 BIM360 integration 은 성공했다. 이 error 로그는 정상적인 "사용자 재인증 필요" 상황이 인프라 레벨의 error 로 노출되는 문제이다.

Technical Analysis#

Code Path#

  • Entry point: lib/cupix/cron/integration.rb:3 — 4시간 주기 크론이 refresh 만료 예정 integration 을 순회.
  • Scope: app/models/concerns/statable/integration.rb:12not_failed 이면서 refresh_token_expired_at < 1.days.since 인 레코드가 대상.
  • Refresh dispatch: app/repositories/integration_repository.rb:103-142 — provider 별 operation 을 선택해 refresh_token 호출.
  • Failure point: app/operations/bim360_operation.rb:60-70 — Autodesk 400 응답을 rescue.
lib/cupix/cron/integration.rb:3-15ruby
def self.renew_before_expiration
  candidates = Integration.refresh_token_due_to_expire
  return nil if candidates.blank?

  candidates.each do |model|
    integration_repository = IntegrationRepository.new(model)
    integration_repository.refresh_token
  rescue StandardError => e
    Cupix::Logger.error("[Cupix::Cron::Integration] failed to renew integration: #{e.message}")

    next
  end
end
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
app/operations/bim360_operation.rb:50-70ruby
def self.refresh_token(refresh_token, region)
  data = {
    grant_type: 'refresh_token',
    refresh_token: refresh_token,
    redirect_uri: $OAUTH[:autodesk_forge][:bim360][:redirect_uri]
  }
  header = {
    authorization: "Basic #{$OAUTH[:autodesk_forge][:bim360][:token]}"
  }

  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']}"
    )

기대 동작 vs 실제 동작:

  • 기대: Autodesk 가 400 을 돌려주면 response['developerMessage'] 로 사람이 읽을 수 있는 원인(예: "The refresh token is invalid")을 로그와 예외 사유에 남기고, 상위 크론은 해당 integration 을 failed state 로 전이한 뒤 다음 candidate 로 넘어간다.
  • 실제: Autodesk Forge v2 OAuth 응답은 표준 OAuth2 오류 형식이라 developerMessage 필드가 없다(응답 예: {"error":"invalid_grant","error_description":"..."}). 따라서 로그의 원인 자리에 빈 문자열이 들어가 "BIM360 refresh_token failed: - error: 400 Bad Request" 가 되고, integration 도 failed state 로 정상 전이되지만, 운영팀 입장에서는 어떤 400 인지(만료된 토큰인지, 잘못된 client credential 인지 등) 로그만으로 구분할 수 없다.

또한 이 error 로그는 개별 사용자의 재인증 필요 상황(정상 운영 경로)임에도 status:error 로 남아 error-sweeper 가 클러스터를 생성하는 원인이 되고 있다. get_token(line 32, 39)은 class:/function: kwargs 를 넘기지만, refresh_token(line 66, 72)은 이를 누락하고 있어 로그 인덱싱상 @class/@function 태그도 붙지 않는다.

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-migration-worker "BIM360"

같은 크론 배치(2026-07-13 13:07:19 KST) 로그 발췌:

text
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
13:07:19  info   [Integration] refresh token for bim360 integration(7299) - state: active, ...
13:07:19  info   [Integration] refresh token for bim360 integration(7298) - state: inactive, ...
13:07:19  info   [Integration] refresh token for bim360 integration(7295) - state: active, ...
13:07:19  error  BIM360 refresh_token failed:  - error: 400 Bad Request
13:07:19  error  [Integration] failed to refresh token for bim360 integration(2332) - state: failed, error_message: BIM360 Authentication failed:
13:07:19  error  [Cupix::Cron::Integration] failed to renew integration: BIM360 Authentication failed:
13:07:19  info   [Integration] Successfully refreshed token for bim360 integration(7299) - state: active, expired_at: 2026-07-13 05:07:15 UTC, ...
13:07:19  info   [Integration] Successfully refreshed token for bim360 integration(7298) - state: inactive, ...
13:07:19  info   [Integration] Successfully refreshed token for bim360 integration(7295) - state: active, ...

포인트:

  • 같은 크론 배치에서 4개 BIM360 integration 중 3개(7295/7298/7299) 성공, 1개(2332) 실패. Autodesk 서비스 전역 장애면 모두 실패해야 하므로 integration 개별 문제임이 확인된다.
  • integration 2332 는 refresh 직전 active 였고, 실패 직후 failed 로 전이 (integration_repository.rb:138 failed_state!).
  • 이전 크론(2026-07-13 09:07:34 KST) 에서도 integration 1160, 1163 이 동일한 방식으로 실패하고, 1581 은 성공.

integration 2332 이력 검색:

text
service:cupixworks-migration-worker "integration(2332)"

결과: 최근 14일 내 로그가 위 2건뿐. 이전 성공/실패 이력이 사실상 없으므로 해당 계정이 오랫동안 사용되지 않다가 refresh_token 이 Autodesk 측에서 무효화되었을 가능성이 높다(추정 — 실제 Autodesk 응답 body 는 로그되지 않아 확정 불가).

추가 확인 불가 항목refresh_token_response_body 는 DB 컬럼에는 저장되지만(integration_repository.rb:137), Datadog 로그에는 남지 않아 이 세션 내에서 실제 error/error_description 값은 확인 불가. Kibana/DB 조회 시 Integration#refresh_token_response_body 컬럼에서 확인 가능.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 개별 integration 의 refresh_token 이 Autodesk 측에서 무효화(사용자 revoke / 자연 만료) 되어 400 응답 같은 크론 배치에서 3/4 BIM360 integration 은 성공 (로그 13:07:19). 실패 integration(2332) 은 즉시 failed state 로 전이. 09:07 배치에서도 동일 패턴(1/3 실패). 실제 응답 body(error_description) 미로깅으로 확정 불가 Confirmed (사실상; error_description 미확인)
H2 Autodesk Forge OAuth 엔드포인트 장애 (전역 outage) 400 응답이 반복 발생 같은 시각 동일 엔드포인트로 3개 integration 은 200 응답(로그 Successfully refreshed token). 전역 장애면 모두 실패해야 함 Rejected
H3 tesla 의 client credential($OAUTH[:autodesk_forge][:bim360][:token]) 이 잘못 설정됨 400 Bad Request 는 client credential 문제일 수 있음 client credential 오류라면 모든 integration 이 실패해야 하는데 대부분 성공. Basic auth header 는 동일 값 재사용 Rejected
H4 tesla 쪽 요청 페이로드(redirect_uri 등) 불일치로 400 400 응답, redirect_uri 를 refresh 시에도 포함(bim360_operation.rb:54) 동일 코드 경로에서 다른 integration 은 성공. 페이로드 문제라면 전부 실패 Rejected
H5 check_refresh_request (Redis setnx) lock 경합으로 400 유발 integration_repository.rb:124 에 lock 체크 있음 lock 실패 시에는 ARG10060 (Parameter) 예외로 조기 raise 되어 Autodesk 호출 자체가 발생하지 않음. 로그 상 Autodesk 400 응답이 확실히 관측됨 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

없음. 시스템 결함이 아니라 개별 사용자 계정의 재인증 필요 상황이다. 운영팀은 integration state 가 failed 로 전이된 BIM360 integration(예: 2332, 1160, 1163) 소유자에게 Autodesk 재인증을 안내하면 된다.

단기 개선 (1주 이내)#

  1. Autodesk OAuth 응답 파싱 정합화 — app/operations/bim360_operation.rb:63-70

    • Autodesk Forge v2 는 표준 OAuth2 오류 응답 형식(error, error_description) 을 사용한다(주석의 참고 URL https://forge.autodesk.com/en/docs/oauth/v2/reference/http/refreshtoken-POST/ 에도 명시). response['developerMessage'] 대신 response['error_description'] || response['error'] || response['developerMessage'] 형태로 대체해 실제 원인을 로그에 남긴다.
    • 방향: 로깅 개선 및 진단성 향상. 코드 변경은 문자열 추출부만 최소 수정.
  2. refresh_token 실패 로그의 레벨 재검토 — app/operations/bim360_operation.rb:66integration_repository.rb:140

    • Autodesk invalid_grant (사용자 revoke / 만료) 는 정상적인 사용자 재인증 시나리오이며, 애플리케이션 결함이 아니다. 이 케이스는 warn 으로 다운그레이드해 status:error 알림 채널에 노이즈를 만들지 않도록 한다.
    • 범위 유의: rescue StandardError 전체를 warn 으로 낮추면 Autodesk 실제 장애가 묻힌다. RestClient::BadRequest (400) 이고 응답 body 의 errorinvalid_grant 인 경우에만 warn, 나머지는 error 유지. (memory 의 "Scope warn-level downgrades to the specific exception class" 원칙과 일치.)
  3. 로그 kwargs 통일 — app/operations/bim360_operation.rb:66

    • 같은 파일의 get_token (line 32, 39) 은 class: self.name, function: __method__ 를 넘긴다. refresh_token 의 line 66, 72 는 이를 누락해 Datadog @class/@function 태그가 붙지 않아 대시보드/알림 필터링이 어렵다. 시그니처만 맞춘다.

장기 개선 (재발 방지)#

  1. Integration state transition 알림 파이프라인
    • activefailed 로 전이된 external integration 을 소유 팀에 안내하는 별도 워크플로우(Slack DM, in-app banner 등) 를 정의해, 로그 error 로 감지하지 않아도 사용자가 재인증할 수 있게 한다. refresh_token_failed_at / refresh_token_response_body 컬럼이 이미 저장되고 있으므로 이를 소비하는 웹훅/알림 잡을 추가.
  2. Provider별 401/400 응답 스키마 문서화
    • Bim360Operation 뿐 아니라 ProcoreOperation, PlangridOperation, ReviztoOperation 도 동일 패턴으로 provider 별 응답 필드를 하드코딩한다(grep refresh_token app/operations 참조). 각 provider 의 표준 오류 스키마를 정리한 헬퍼(OauthOperation.extract_error_reason) 를 도입해 유사한 로그 공백 이슈를 예방한다.

Monitoring#

writing-datadog-monitoring-queries 규칙에 따라 dashboard timeseries widget 에 그대로 사용 가능한 query 만 사용한다(| stats, count by, threshold suffix 미사용).

  • BIM360 refresh 실패 카운트 (기존 노이즈 감시용):
text
service:cupixworks-migration-worker status:error "BIM360 refresh_token failed"
  • Integration state 가 failed 로 전이된 BIM360 케이스(사용자 재인증 유도 지표):
text
service:cupixworks-migration-worker status:error "failed to refresh token for bim360" "state: failed"
  • Autodesk 전역 장애 조기 감지 (성공/실패 비율 확인용, 성공 로그와 병행 관측):
text
service:cupixworks-migration-worker "Successfully refreshed token for bim360"

권장 알림 임계값: 크론 배치(4시간 주기) 당 실패 카운트가 성공 카운트를 초과하거나, 첫 번째 쿼리가 1시간 내 5건 이상일 때 조사. 개별 실패(현재 배치당 1건) 는 정상 사용자 revoke 로 간주하고 알림하지 않는다.

Risk Assessment#

  • Risk level: low — 시스템 결함 아님, 개별 사용자 계정 재인증 필요. 로깅 개선 위주 수정.
  • 예상 복잡도: trivialbim360_operation.rb 의 문자열 추출 및 rescue narrowing 만으로 대응 가능. 단기 개선 3개 모두 한 파일(+로그 라벨) 범위.