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#
- 2026-07-13 09:07 KST — 이전 크론 사이클.
integration(1160),integration(1163)에 대해 동일한BIM360 refresh_token failed: - error: 400 Bad Request발생, integration state →failed전이. - 2026-07-13 13:07:19 KST — 이 클러스터의 대표 로그 발생.
integration(2332)(직전 상태active) 에 대해 refresh 시도, Autodesk 가 400 Bad Request 응답. state →failed. 동일 배치의integration(7295/7298/7299)는 성공. - 감지 — error-sweeper collector 가 fingerprint
bf5327d789818de62530896fd1ed1ee2로 클러스터 파일 생성.
Error Log#
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:12—not_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.
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
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
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 을failedstate 로 전이한 뒤 다음 candidate 로 넘어간다. - 실제: Autodesk Forge v2 OAuth 응답은 표준 OAuth2 오류 형식이라
developerMessage필드가 없다(응답 예:{"error":"invalid_grant","error_description":"..."}). 따라서 로그의 원인 자리에 빈 문자열이 들어가"BIM360 refresh_token failed: - error: 400 Bad Request"가 되고, integration 도failedstate 로 정상 전이되지만, 운영팀 입장에서는 어떤 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 쿼리:
service:cupixworks-migration-worker "BIM360"
같은 크론 배치(2026-07-13 13:07:19 KST) 로그 발췌:
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:138failed_state!). - 이전 크론(2026-07-13 09:07:34 KST) 에서도 integration 1160, 1163 이 동일한 방식으로 실패하고, 1581 은 성공.
integration 2332 이력 검색:
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주 이내)#
-
Autodesk OAuth 응답 파싱 정합화 —
app/operations/bim360_operation.rb:63-70- Autodesk Forge v2 는 표준 OAuth2 오류 응답 형식(
error,error_description) 을 사용한다(주석의 참고 URLhttps://forge.autodesk.com/en/docs/oauth/v2/reference/http/refreshtoken-POST/에도 명시).response['developerMessage']대신response['error_description'] || response['error'] || response['developerMessage']형태로 대체해 실제 원인을 로그에 남긴다. - 방향: 로깅 개선 및 진단성 향상. 코드 변경은 문자열 추출부만 최소 수정.
- Autodesk Forge v2 는 표준 OAuth2 오류 응답 형식(
-
refresh_token실패 로그의 레벨 재검토 —app/operations/bim360_operation.rb:66및integration_repository.rb:140- Autodesk
invalid_grant(사용자 revoke / 만료) 는 정상적인 사용자 재인증 시나리오이며, 애플리케이션 결함이 아니다. 이 케이스는warn으로 다운그레이드해status:error알림 채널에 노이즈를 만들지 않도록 한다. - 범위 유의:
rescue StandardError전체를 warn 으로 낮추면 Autodesk 실제 장애가 묻힌다.RestClient::BadRequest(400) 이고 응답 body 의error가invalid_grant인 경우에만 warn, 나머지는 error 유지. (memory 의 "Scope warn-level downgrades to the specific exception class" 원칙과 일치.)
- Autodesk
-
로그 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태그가 붙지 않아 대시보드/알림 필터링이 어렵다. 시그니처만 맞춘다.
- 같은 파일의
장기 개선 (재발 방지)#
- Integration state transition 알림 파이프라인
active→failed로 전이된 external integration 을 소유 팀에 안내하는 별도 워크플로우(Slack DM, in-app banner 등) 를 정의해, 로그 error 로 감지하지 않아도 사용자가 재인증할 수 있게 한다.refresh_token_failed_at/refresh_token_response_body컬럼이 이미 저장되고 있으므로 이를 소비하는 웹훅/알림 잡을 추가.
- 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 실패 카운트 (기존 노이즈 감시용):
service:cupixworks-migration-worker status:error "BIM360 refresh_token failed"
- Integration state 가
failed로 전이된 BIM360 케이스(사용자 재인증 유도 지표):
service:cupixworks-migration-worker status:error "failed to refresh token for bim360" "state: failed"
- Autodesk 전역 장애 조기 감지 (성공/실패 비율 확인용, 성공 로그와 병행 관측):
service:cupixworks-migration-worker "Successfully refreshed token for bim360"
권장 알림 임계값: 크론 배치(4시간 주기) 당 실패 카운트가 성공 카운트를 초과하거나, 첫 번째 쿼리가 1시간 내 5건 이상일 때 조사. 개별 실패(현재 배치당 1건) 는 정상 사용자 revoke 로 간주하고 알림하지 않는다.
Risk Assessment#
- Risk level: low — 시스템 결함 아님, 개별 사용자 계정 재인증 필요. 로깅 개선 위주 수정.
- 예상 복잡도: trivial —
bim360_operation.rb의 문자열 추출 및 rescue narrowing 만으로 대응 가능. 단기 개선 3개 모두 한 파일(+로그 라벨) 범위.