ES /docs

BimRevisionService::run | error: "HTTP request failed"

RCA: BimRevisionService::run | error: "HTTP request failed"

Overview#

What Happened#

2026-06-10 16:02 KST cupixworks-any-bimrevision-agent 에서 BIM revision 24050에 대한 비교 작업을 SQS로 수신하여 처리하던 중, 부모 BIM(id=19801) 메타데이터 조회 단계에서 Tesla API가 403 Bim not found (ENT4000: Record has trashed)를 반환하면서 OpenAPI generated client가 HttpError('HTTP request failed')를 throw하여 작업이 실패했다. 사용자가 동일 BIM을 약 2분 전(15:59:58 KST)에 trash로 옮긴 직후 큐에 적재된 revision 메시지가 처리되며 발생한 race condition이다. 발생 횟수는 1건.

Quick Facts#

Field Value
exception.class HttpError (OpenAPI generated client)
exception.message HTTP request failed
top_frame bim-revision-service.ts:189 (catch block in BimRevisionService.run)
failing_call cupixApi.bim.get(19801)[403] GET /api/v1/bims/19801
upstream_error Cupix::Errors::NotFound (code ENT4000, "Record has trashed")
runtime Node.js (cupixworks agents monorepo)
env production, region us-west-2
tenant cupix

Affected Teams#

Team / Domain Error Count Impact
ikukbo (BIM revision agent) 1 BIM revision 24050 비교 결과가 Error 상태로 마킹됨. 사용자 가시 영향: 해당 revision의 비교/diff UI가 비어 있거나 Error 표시 (uncertain — UI 검증 필요).

Timeline#

  1. 2026-06-10 15:55:16 ~ 15:59:28 KST — BIM 19801에 대해 정상적인 GET/PUT/POST 호출이 다수 발생 (translation, meta update 등).
  2. 2026-06-10 15:59:58 KST[204] PUT /api/v1/bims/19801/trash 호출로 BIM이 trash 상태로 이동.
  3. 2026-06-10 16:02:06 KST — bim-revision-agent가 SQS 메시지(id: 24050)를 수신, BaseService runByMessage 시작.
  4. 2026-06-10 16:02:08 KSTBimRevisionService::run | si_trace_id: 6651d331-... 로그. loadBimRevision(24050)/loadPreviousBimRevision(24049) 모두 200 OK.
  5. 2026-06-10 16:02:09 KST — Tesla API: [200] GET /api/v1/bim_revisions/24050[200] GET /api/v1/bim_revisions/24049.
  6. 2026-06-10 16:02:10 KST — Tesla API: [403] GET /api/v1/bims/19801 (Bim not found, ENT4000: Record has trashed).
  7. 2026-06-10 16:02:08-09 KST — agent: BimRevisionService::run | error: "HTTP request failed" (catch block 진입, ec.message만 직렬화).
  8. 2026-06-10 16:02:11 KST[200] PUT /api/v1/bim_revisions/24050 (state: Error). SQS 메시지 삭제, workspace 정리.

Error Log#

Datadog Logs

text
BimRevisionService::run | error: "HTTP request failed"

Impact#

  • Service: cupixworks-any-bimrevision-agent
  • Team: ikukbo
  • 발생 횟수: 1
  • 최초 발생: 2026-06-10 16:02 KST
  • 최근 발생: 2026-06-10 16:02 KST
  • 사용자 영향: BIM revision 24050의 bim_comparison_stateError로 마킹됨. 단, 부모 BIM(19801)이 trash 상태이므로 사용자는 이미 해당 BIM을 사용하지 않는 상태. 실질적 작업 손실은 없으나 retry 무한 실패가 발생할 수 있음 (uncertain — DLQ/재시도 정책 검증 필요).

Root Cause Summary#

사용자가 BIM 19801을 trash로 옮긴 직후(15:59:58 KST), 같은 BIM을 부모로 갖는 BIM revision 24050에 대한 비교 작업 SQS 메시지가 약 2분 뒤(16:02:06 KST) 큐에서 dequeue되어 처리되었다. BimRevisionService.createCPBimByCPBimRevisioncupixApi.bim.get(19801)을 호출하자 Tesla API의 BaseRepository#show가 active scope에서는 BIM을 찾지 못하고 in_trash scope에서 찾아 Cupix::Errors::NotFound(code: 'ENT4000', reason: 'Bim not found')를 raise했다. client_error_controller가 이를 HTTP 403으로 매핑하여 응답했고, OpenAPI generated TypeScript client의 HttpError 클래스는 status code/body와 무관하게 항상 super('HTTP request failed')만으로 Error를 만들기 때문에 agent의 catch block에는 원인을 식별할 수 없는 단순 메시지만 남았다. agent에는 trashed/404/403 상황에 대한 별도 분기가 없으며, revision은 일반 에러로 간주되어 bim_comparison_state: Error로 종결되었다.

Technical Analysis#

Code Path#

Entry: SQS message → BaseService.runByMessageBimRevisionService.run(targetId=24050, msgObject) (bim-revision-service.ts:150)

1. revision 두 건 조회 (성공)bim-revision-service.ts:166-167

applications/agents/packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:160-170typescript
try {
    if (previousTargetId === undefined || previousTargetId === null || isNaN(Number(previousTargetId))) {
        logger.error('BimRevisionService::run | invalid previousTargetId: %s, msgObject: %s', previousTargetId, JSON.stringify(msgObject));
        throw new Error(`Invalid previousTargetId: ${previousTargetId}`);
    }

    await this.loadBimRevision(targetId);
    await this.loadPreviousBimRevision(previousTargetId);

두 호출 모두 [200] GET /api/v1/bim_revisions/24050, [200] GET /api/v1/bim_revisions/24049로 성공.

2. createTargetModels → bim.get (실패 지점)bim-revision-service.ts:285:313

applications/agents/packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:282-296typescript
private createTargetModels = async (cpBimRevision: CPBimRevision): Promise<void> => {
    logger.debug('BimRevisionService::createTargetModels | begin - bimRevisionId: %d', cpBimRevision.id);
    const cpBim = await this.createCPBimByCPBimRevision(cpBimRevision);  // ← 여기서 throw
    logger.debug('BimRevisionService::createTargetModels | cpBim: %s', cpBim.id);
    if (cpBim.useSiteinsights) {
        await this.loadAllElements(cpBim);
        if (!cpBimRevision.validateDuplicatedBimExternalIds()) {
            throw new Error('duplicated bim_external_ids found');
        }
        await this.loadAllLevels(cpBim);
    }
};
applications/agents/packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:305-317typescript
private createCPBimByCPBimRevision = async (cpBimRevision: CPBimRevision): Promise<CPBim> => {
    logger.debug('BimRevisionService::createCPBimByCPBimRevision | begin - bimRevisionId: %d', cpBimRevision.id);
    if (!cpBimRevision.srvBim?.id) {
        logger.error('BimRevisionService::createCPBimByCPBimRevision | srvBim is undefined - bimRevisionId: %d', cpBimRevision.id);
        throw new Error('srvBim is undefined');
    }

    const srvBim = await this.cupixApi.bim.get(cpBimRevision.srvBim.id);  // ← Tesla API 호출
    logger.debug('BimRevisionService::createCPBimByCPBimRevision | end - bim_id: %d', srvBim.id);

    return new CPBim(srvBim, cpBimRevision);
};

3. OpenAPI client가 throw하는 HttpErrorcupix-api/client/openapi/typescript-node/tesla-v1/api/apis.ts:197-202

cupix-api/client/openapi/typescript-node/tesla-v1/api/apis.ts:197-202typescript
export class HttpError extends Error {
    constructor (public response: http.IncomingMessage, public body: any, public statusCode?: number) {
        super('HTTP request failed');  // ← message에는 status code/body가 들어가지 않음
        this.name = 'HttpError';
    }
}

statusCode/body는 HttpError 인스턴스에 보존되지만 super('HTTP request failed')로 인해 error.message에는 항상 동일 문자열만 남는다.

4. Tesla API: BIM이 trash 상태일 때 ENT4000 raisetesla/app/repositories/base_repository.rb:351-356

tesla/app/repositories/base_repository.rb:350-356ruby
if model.nil?
  if self.where(attrs).in_trash.present?
    raise Cupix::Errors::NotFound.new(code: 'ENT4000', reason: "#{current_class.name} not found")
  else
    raise Cupix::Errors::NotFound.new(code: 'ARG10002', reason: "#{current_class.name} not found")
  end
end
tesla/app/controllers/concerns/client_error_controller.rb:26-27ruby
rescue_from Cupix::Errors::PermissionDenied, with: :permission_denied_403_error
rescue_from Cupix::Errors::NotFound, with: :not_found_403_error

Cupix::Errors::NotFound는 controller 레벨에서 HTTP 403 으로 매핑된다 (코드 ENT4000 = "Record has trashed", tesla/config/error_code/entity.yml:49-50).

5. Failure point: catch block 메시지 직렬화bim-revision-service.ts:188-191

applications/agents/packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:188-192typescript
} catch (ec: any) {
    logger.error('BimRevisionService::run | error: %s', JSON.stringify(ec.message));
    await this.updateBimRevisionBimComparisonState(targetId, TESLA.UpdateBimRevisionRequest.BimComparisonStateEnum.Error, this.errorCode);
}

ec.message'HTTP request failed'이므로 status code/응답 body가 모두 사라진다. ec는 HttpError 인스턴스이므로 ec.statusCode, ec.body에는 진짜 원인이 들어 있지만 로그에는 남지 않는다.

기대 동작 vs 실제 동작:

  • 기대: BIM이 trash로 이동된 경우 retry/재처리가 무의미하므로 trashed로 인지하고 SQS 메시지를 ack한 뒤 명확한 사유(error_code: BimRevisionParentBimTrashed 등)로 종결.
  • 실제: 일반 HTTP 실패와 구분 없이 error_code: Default로 종결되며, 로그에는 status code도 없는 HTTP request failed만 남아 운영자가 사후 진단 시 Tesla API 로그와 timestamp를 별도로 매칭해야 한다.

Log Evidence#

Datadog 쿼리 (agent 측, 실패 trace):

text
service:cupixworks-any-bimrevision-agent "BimRevisionService"
time: 2026-06-10T06:30:00Z ~ 2026-06-10T07:30:00Z

핵심 agent 로그 (시간순):

json
{ "ts": "2026-06-10 16:02:08", "status": "info",  "msg": "BaseService::runByMessage | id: 24050" }
{ "ts": "2026-06-10 16:02:08", "status": "info",  "msg": "CupixAuth::setSession | session_id: 3b07b9fca0894313e64c9de0e177360791145182" }
{ "ts": "2026-06-10 16:02:08", "status": "info",  "msg": "BimRevisionService::run | si_trace_id: 6651d331-8618-4d66-bc46-74fc88bff2ed" }
{ "ts": "2026-06-10 16:02:08", "status": "error", "msg": "BimRevisionService::run | error: \"HTTP request failed\"" }
{ "ts": "2026-06-10 16:02:09", "status": "info",  "msg": "BimRevisionService::REVISE-END | {\"src_revision_id\":24050,\"prev_revision_id\":24049,\"si_trace_id\":\"6651d331-8618-4d66-bc46-74fc88bff2ed\",\"elapsed_ms\":929,\"status\":\"error\",\"error_message\":\"HTTP request failed\",\"counts\":{\"total\":0,\"modified\":0,\"removed\":0,\"exist\":0}}" }

전체 처리 시간 929ms — loadBimRevision, loadPreviousBimRevision 이후 첫 번째 bim.get에서 즉시 실패했음을 시사.

Datadog 쿼리 (Tesla API 측, BIM 19801 호출 이력):

text
service:cupixworks-api "bims/19801"
time: 2026-06-10T06:00:00Z ~ 2026-06-10T07:30:00Z

핵심 API 로그:

text
2026-06-10 15:59:28  [200] PUT /api/v1/bims/19801 (Api::V1::BimsController#update)
2026-06-10 15:59:58  [204] PUT /api/v1/bims/19801/trash (Api::V1::BimsController#trash)   ← BIM trashed
2026-06-10 16:02:10  [403] GET /api/v1/bims/19801 (Api::V1::BimsController#show)          ← agent의 호출
                       error: { "code": "ENT4000", "message": "Bim not found",
                                "class": "Cupix::Errors::NotFound" }

agent의 bim.get(19801)에 대해 Tesla API는 Cupix::Errors::NotFound(ENT4000)를 raise → controller가 403으로 매핑 → OpenAPI client가 HttpError('HTTP request failed')로 throw.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 부모 BIM이 trash로 이동된 직후 SQS 메시지가 처리되면서 cupixApi.bim.get(19801)ENT4000 (403)을 받았고, OpenAPI client가 message를 HTTP request failed로 평탄화하여 catch block까지 전달됨 Tesla API 로그 [204] PUT /api/v1/bims/19801/trash (15:59:58)와 [403] GET /api/v1/bims/19801 (16:02:10, code ENT4000); base_repository.rb:351-353에서 trash 상태에 대해 ENT4000 raise; client_error_controller.rb:27이 NotFound를 403으로 매핑; apis.ts:199의 super('HTTP request failed') Confirmed
H2 bim-revision-agent의 인증 토큰 만료/세션 문제로 인해 일반 401/403 응답을 받음 setSession은 16:02:08에 정상 수행됨 같은 세션으로 bim_revisions/24050, bim_revisions/24049 GET이 직전에 200으로 성공; 403 응답 본문에 인증 오류가 아닌 Bim not found ENT4000이 포함됨 Rejected
H3 Tesla API 일시적 5xx/timeout으로 인한 실패 HTTP request failed라는 generic message는 timeout과도 호환됨 Tesla API 측 동일 시각 동일 path에 명시적 [403] GET /api/v1/bims/19801 로그가 존재; 5xx/timeout 흔적 없음; 처리 시간 929ms로 timeout 임계 이하 Rejected
H4 Forge URN 동일성 검증 실패(Cannot compare revisions with identical forge URN) 등 비-HTTP exception 같은 catch block이 일반 Error도 처리 그 경우 ec.messageCannot compare ...이지 HTTP request failed가 아님 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  1. bim-revision-service.ts:188-191의 catch block 보강ec가 OpenAPI generated HttpError 인스턴스인지 검사하여 ec.statusCode, ec.body 까지 함께 logging. JSON.stringify(ec.message) 한 줄로 모든 정보를 잃지 않도록 한다.

    • 근거: 동일 fingerprint의 다음 실패가 발생했을 때 어떤 endpoint가 어떤 status로 실패했는지 한 번에 식별 가능해야 RCA가 빨라진다. 현재는 Tesla API 로그와 si_trace_id로 매칭해야만 원인 파악 가능.
  2. createCPBimByCPBimRevision (bim-revision-service.ts:305-317) — 부모 BIM trashed 분기 추가bim.get 호출에서 403 + ENT4000 응답인 경우 BimRevisionParentBimTrashed 같은 명시적 errorCode로 종결하고, 무한 retry되지 않도록 SQS 메시지를 ack 처리.

    • 근거: 사용자가 BIM을 trash로 옮긴 시점 이후 큐에 남아 있던 revision 비교 작업은 의미가 없으므로 명시적으로 무효 처리하는 것이 합리적이다.

단기 개선 (1주 이내)#

  • 공통 OpenAPI client 에러 매퍼 도입@agents/api 패키지에 HttpError를 래핑해 error.message[status] ${url}: ${body.message} 형태로 재구성하는 helper를 두고, 모든 agent service의 catch block이 이를 사용하도록 한다. 동일 generic 메시지가 다른 agent들(forge-agent, complete-agent 등)에서도 빈번할 가능성이 높다 (uncertain — 확인 필요).
  • Datadog log facet 추가error_code: ENT4000 같은 BIM/revision 관련 client error response를 agent 로그 측에서도 facet으로 추출. 현재는 message string에만 묻혀 있어 검색이 어렵다.

장기 개선 (재발 방지)#

  • Trash event consumption — BIM trashing이 완료되면 해당 BIM에 연관된 in-flight SQS 메시지를 무효화하는 cleanup 단계 도입 (예: trash 시 BimRevisionsService side에서 bim_comparison_state를 별도 상태로 마킹하여 agent가 일찍 분기 종료).
  • Idempotent retry policy — agent 측 SQS visibility timeout/redrive policy를 점검해 trashed 상태 같은 사용자 인입 변경에 대해 무한 재시도가 발생하지 않도록 검증 (uncertain — 현재 redrive policy 미확인).

Monitoring#

추가/확인할 Datadog 지표 및 검색 쿼리:

agent 에러 발생률 (timeseries):

text
sum:logs.hits{service:cupixworks-any-bimrevision-agent,status:error,@environment:production}.as_count()

HTTP request failed 메시지 발생률 (이번 cluster의 fingerprint 추적):

text
sum:logs.hits{service:cupixworks-any-bimrevision-agent,status:error,@environment:production,"HTTP request failed"}.as_count()

Tesla API에서 trash 관련 ENT4000 응답 수 (root-cause 신호):

text
sum:logs.hits{service:cupixworks-api,@environment:production,"ENT4000"}.as_count()

BIM revision-agent의 처리 종결 상태별 분포 (REVISE-END 패턴):

text
sum:logs.hits{service:cupixworks-any-bimrevision-agent,@environment:production,"REVISE-END","status:error"}.as_count()

추가 알림 권장: cupixworks-any-bimrevision-agent의 status=error 5분 이동 평균이 baseline의 N배를 초과할 경우 Slack 알림.

Risk Assessment#

  • Risk level: low — 1회 발생, 사용자 데이터 손실 없음, BIM은 이미 사용자가 trash로 이동한 상태.
  • 예상 복잡도: standard — fix는 catch block logging 보강 + ENT4000 분기 처리 두 곳, 영향 범위는 단일 agent.
  • 재발 가능성: medium — 사용자가 trash 직후 큐에 남은 revision message가 dequeue되는 timing 문제는 언제든 다시 발생할 수 있음.