ES /docs

ForgeService::handlingMessageErrors | Errors in error handling - {"response":{"statusCode":403,"body

RCA: ForgeService::handlingMessageErrors | Errors in error handling (403 PERM33000)

Overview#

What Happened#

2026-05-12 06:15:52–06:15:56 UTC 사이 cupixworks-any-forge-agent(ap-northeast-1)에서 BIM 업로드 SQS 메시지 처리 중 원본 에러가 발생했고, 이어지는 에러 핸들러가 tesla API에 상태 업데이트(PUT /api/v1/bims/{id}, forge_state: Error)를 시도했으나 동일한 403 PERM33000 Cupix::Errors::PermissionDenied ("Updating entity on billing expired team/workspace is not allowed")가 반환되어 핸들러 내부의 중첩 try/catch에서 "Errors in error handling" 로그로 기록되었다. 총 2건 (bim id 219, 220), 동일 테넌트(cupix)에서 발생.

Quick Facts#

Field Value
exception.class HttpError
exception.message Updating entity on billing expired team/workspace is not allowed (PERM33000)
top_frame packages/cupix-tesla-forge-agent/src/forge-service.ts:407
runtime Node.js (cupix-tesla-forge-agent)
env production, ap-northeast-1

Affected Teams#

Team / Domain Error Count Impact
cupixworks-any-forge-agent (BIM forge translation) 2 빌링 만료된 team/workspace의 BIM 업로드 요청이 실패 시 agent가 상태를 Error로 반영하지 못함 — BIM forge_state가 원래 값(예: Processing)에 머물 가능성
tesla (cupixworks-api) 2 PERM33000 반환 (정상 동작, 의도된 검증)

Timeline#

  1. 2026-05-12 06:15:52 UTC — bim id 220, bim_revision 253에 대해 runByMessagesPUT /api/v1/bim_revisions/253 → 403 PERM33000
  2. 2026-05-12 06:15:52 UTChandlingMessageErrorsupdateBimForgeState(220, Error) 시도 → 동일한 403 PERM33000 → "Errors in error handling" 로그
  3. 2026-05-12 06:15:56 UTC — bim id 219, bim_revision 252에 대해 동일 시나리오 재현

Error Log#

Datadog Logs

text
ForgeService::handlingMessageErrors | Errors in error handling - {"response":{"statusCode":403,"body":{"result":{"code":"PERM33000","type":"Cupix::Errors::PermissionDenied","reason":"Updating entity on billing expired team/workspace is not allowed","message":"Updating entity on billing expired team/workspace is not allowed"}}, ...,"request":{"uri":{..."pathname":"/api/v1/bims/220",...},"method":"PUT",...}},"body":{"result":{}},"statusCode":403,"name":"HttpError"}

Impact#

  • Service: cupixworks-any-forge-agent
  • 발생 횟수: 2
  • 최초 발생: 2026-05-12T06:15:52.724Z
  • 최근 발생: 2026-05-12T06:15:56.776Z

사용자 관점 영향은 작다. 원본 에러는 빌링 만료라는 의도된 정책 위반으로, 정상적으로 차단되어야 하는 요청이다. 문제는 agent 측 로깅이다 — 정상 운영 시나리오(expired billing)에서 logger.error 두 줄("Errors in error handling" + "Error and message object")이 남아 실제 이상 징후와 구분이 어렵게 된다. 또한 agent가 BIM의 forge_stateError로 기록하지 못하므로, 빌링 만료 테넌트의 BIM 레코드는 중간 상태(예: Processing)에 고착될 수 있다.

Root Cause Summary#

tesla API는 check_updatable_by_billing_state! (lib/cupix/abstract/base.rb:31-43)에서 applied_billing_state == 'expired' 인 엔티티의 모든 업데이트를 PERM33000으로 차단한다. cupix-tesla-forge-agent는 SQS 메시지 처리 중 원본 요청(PUT /api/v1/bim_revisions/{id})이 이 검증에 걸려 403을 받았다. handlingMessageErrors는 실패한 메시지의 상태를 반영하기 위해 updateBimForgeStatePUT /api/v1/bims/{id}를 호출하는데, 같은 빌링 만료 조건 때문에 이 상태 업데이트 호출 또한 동일하게 PERM33000을 반환한다. 에러 핸들러의 중첩 try/catch가 이 2차 예외를 잡아 "Errors in error handling" 로그로 기록한다. 즉, 이 로그는 새로운 결함이 아니라 빌링 만료 테넌트에서 예측 가능한 운영 시나리오가 error 레벨로 노출된 것이다.

Technical Analysis#

Code Path#

  • Entry point: forge-service.ts:91await this.runByMessages()
  • Error origin: forge-service.ts:214 or :276cupixApi.bimRevision.update(bimRevisionId, ...) returns 403
  • Error handler entry: forge-service.ts:93await this.handlingMessageErrors(error)
  • Failure point (inner): forge-service.ts:405updateBimForgeState(bimId, Error) calls PUT /api/v1/bims/{id} which also returns 403
  • Logged at: forge-service.ts:407logger.error('ForgeService::handlingMessageErrors | Errors in error handling - %s', JSON.stringify(error))
  • Upstream validator: tesla/lib/cupix/abstract/base.rb:38
packages/cupix-tesla-forge-agent/src/forge-service.ts:90-95typescript
try {
    await this.runByMessages();
} catch (error) {
    await this.handlingMessageErrors(error);
}
packages/cupix-tesla-forge-agent/src/forge-service.ts:390-414typescript
private handlingMessageErrors = async (error: any): Promise<void> => {
    const errorAndMessage = { error: error, sqsMessage: {} };
    if (this.messageInProcess) {
        errorAndMessage.sqsMessage = { MessageId: ..., Attributes: ... };
        const apiErrorObject = this.getApiErrorToDeleteMessage(error);
        if (apiErrorObject != undefined || this.checkReceiveCountToDeleteMessage()) {
            try {
                errorAndMessage.error = apiErrorObject;
                await this.deleteByMessage(this.messageInProcess);
                if (this._cpBimInProcess != undefined)
                    await this.updateBimForgeState(this._cpBimInProcess.id, TESLA.UpdateBimRequest.ForgeStateEnum.Error);
            } catch (error) {
                logger.error('ForgeService::handlingMessageErrors | Errors in error handling - %s', JSON.stringify(error));
            }
        }
    }
    await this.cleanUpAnythingRelatedModel();
    logger.error('ForgeService::handlingMessageErrors | Error and message object - %s', JSON.stringify(errorAndMessage));
    resetLogMeta();
};
packages/cupix-tesla-forge-agent/src/forge-service.ts:461-468typescript
private updateBimForgeState = async (bimId: number, state: TESLA.UpdateBimRequest.ForgeStateEnum): Promise<void> => {
    logger.debug('ForgeService::updateBimForgeState | begin');
    logger.debug('ForgeService::updateBimForgeState | state: %s', state);
    await this.cupixApi.bim.update(bimId, {
        forge_state: state
    });
    logger.debug('ForgeService::updateBimForgeState | end');
};
tesla/lib/cupix/abstract/base.rb:31-43ruby
def check_updatable_by_billing_state!
  Cupix::Logger.debug("[check_updatable_by_billing_state!] begins on #{self.model.class.name}", ...)

  if self.current_user.present? && self.model.present? && (self.model.respond_to?(:workspace_id) && self.model.respond_to?(:team_id)) && self.current_user.team_id != TeamRepository.admin_team_id && self.model.respond_to?(:applied_billing_state)
    if self.model.applied_billing_state == 'expired'
      raise Cupix::Errors::PermissionDenied.new(code: 'PERM33000', reason: 'Updating entity on billing expired team/workspace is not allowed')
    end
  end
end

기대 동작: 빌링 만료 테넌트의 BIM 업데이트 요청은 차단되어야 함 (정책). 실제 동작: 정책은 정상 작동하나 agent 로깅이 이 상황을 error 레벨로 기록하여, agent는 BIM forge_stateError로 반영할 수 없고 운영 대시보드에서는 에러로 노출됨.

Log Evidence#

Datadog query:

text
service:cupixworks-any-forge-agent "ForgeService::handlingMessageErrors"

Time range: 2026-05-12T05:00:00Z ~ 2026-05-12T07:00:00Z

총 4건 로그 (2건의 에러 × 각 "Errors in error handling" + "Error and message object"). 핵심 원문:

json
{
  "error": {
    "statusCode": 403,
    "requestUriHref": "http://api-tesla.cupix.internal/api/v1/bim_revisions/253?fields[0]=id&...",
    "bodyResult": {
      "code": "PERM33000",
      "type": "Cupix::Errors::PermissionDenied",
      "reason": "Updating entity on billing expired team/workspace is not allowed",
      "message": "Updating entity on billing expired team/workspace is not allowed"
    },
    "modelId": 253
  },
  "sqsMessage": { "MessageId": "6799068d-8ebb-4658-ab11-39fefc49f671", "Attributes": { "ApproximateReceiveCount": "1" } }
}

즉, 원본 실패 URLPUT /api/v1/bim_revisions/253 (bimRevision 업데이트 단계), 이어지는 "Errors in error handling"의 실패 URLPUT /api/v1/bims/220 (forge_state=Error 반영 단계)로, 둘 다 동일한 PERM33000 발생.

이벤트 타임라인:

  • 06:15:52.724Z — bim id 220 / bim_revision 253, ApproximateReceiveCount: 1, PERM33000
  • 06:15:56.776Z — bim id 219 / bim_revision 252, ApproximateReceiveCount: 1, PERM33000

동일 x-request-id 없음, 두 건은 독립 메시지. ApproximateReceiveCount: 1로 첫 수신에서 즉시 실패 — 이전 재시도 흐름 아님.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 빌링이 만료된 team/workspace 소유 BIM의 SQS 메시지를 처리하는 동안 tesla API 정책(PERM33000)이 동작하고, agent의 에러 핸들러가 상태 업데이트까지도 동일 정책에 막혀 중첩 catch에서 error 레벨로 로그를 남긴다 원본 에러 URL bim_revisions/253 과 2차 실패 URL bims/220 모두 응답 body PERM33000 Cupix::Errors::PermissionDenied "Updating entity on billing expired team/workspace is not allowed"; tesla 측 base.rb:38에서 applied_billing_state == 'expired' 시 동 코드로 raise; _cpBimInProcess != undefined 분기에서 updateBimForgeStatePUT /api/v1/bims/{id} 호출 (forge-service.ts:405,461-468) Confirmed
H2 세션/인증 토큰 만료로 403이 발생 요청 헤더에 X-CUPIX-AUTH: session_token:duqhn04caya3,session_id:9811 존재 응답 body가 PERM33000(Permission)이며 401(Unauthorized)이 아님; getApiErrorToDeleteMessage는 401이면 return (forge-service.ts:374), 이번에는 403이 삼켜지지 않고 handler 로직을 그대로 탔음 Rejected
H3 Autodesk Forge API 측 권한 오류가 cupix API로 전달된 것 403 상태 코드 응답 호스트가 api-tesla.cupix.internal이며 body는 cupix 내부 에러 코드 PERM33000. Forge 외부 호출과 무관 Rejected
H4 SQS 재시도 루프에서 메시지가 MaxReceiveCount까지 반복되어 발생 ApproximateReceiveCount: 1 (로그 원문), 첫 수신에서 즉시 handler가 실행됨 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

즉시 수정이 필요한 버그는 아니다. 로그 레벨 조정만으로 충분.

  • 파일: packages/cupix-tesla-forge-agent/src/forge-service.ts:405-408
  • 접근: PERM33000(또는 더 일반적으로 statusCode === 403 + bodyResult.code prefix PERM) 에 한해 logger.errorlogger.warn으로 낮추거나, 내부 try에서 별도로 인지/기록 후 정상 흐름으로 종료. 동시에 "Errors in error handling" 로그 메시지는 진짜 예외(네트워크/5xx)에만 남도록 분기.
  • 근거: MEMORY.md 기록 — 빌링 만료, cross-region, 레이트 리밋 등 예측 가능한 운영 시나리오는 warn이 적절. 현재 구현은 의도된 거절까지 error로 노출되어 알람 노이즈.

단기 개선 (1주 이내)#

  • getApiErrorToDeleteMessage(forge-service.ts:342-378)에서 401 외에도 의도된 정책 위반(PERM33000 등) 을 식별하여, handlingMessageErrorsupdateBimForgeState 호출을 스킵하도록 분기. 빌링 만료 상태에서 forge_state 업데이트 자체가 성공할 수 없으므로 시도 자체를 생략하는 것이 맞다.
  • 동시에 deleteByMessage는 정상 수행되어야 한다 (DLQ로 밀려 재시도 폭주 방지).
  • 중첩 catch의 로그 포맷: Error 객체를 JSON.stringify(error)로 직렬화하면 stack을 잃는다 — MEMORY.md의 로거 규약에 따라 에러 객체를 직접 전달하는 방식으로 변경 검토.

장기 개선 (재발 방지)#

  • Enqueue-time 또는 consumer 측 선행 검증: SQS 메시지 dequeue 직후 해당 team/workspace의 applied_billing_state를 조회하여 expired면 즉시 메시지를 삭제하고 forge_state 갱신 시도를 건너뛰도록. 이렇게 하면 외부 Forge API 인증/버킷 생성 등 불필요한 비용 호출도 예방된다 (forge-service.ts:208-210).
  • tesla API에 "system/internal user"용 bypass 또는 force_state_update 엔드포인트 제공 고려 — 단, 이는 정책 예외이므로 도입 시 보안 검토 필수.
  • 빌링 상태 변화 이벤트(expired 전환)를 agent에 전파하여 진행 중 파이프라인을 사전 취소 처리.

Monitoring#

  • 추가/조정할 알림:
    • 낮출 대상: service:cupixworks-any-forge-agent status:error "ForgeService::handlingMessageErrors" "PERM33000" — warn 채널로 라우팅 또는 알림에서 제외
    • 유지/신규 알림: service:cupixworks-any-forge-agent status:error "ForgeService::handlingMessageErrors" -"PERM33000" — 실제 이상 감지용
  • 대시보드: 빌링 만료 테넌트의 agent 수신 메시지 수 집계 — 메시지 생성 상류 쪽에서 차단되어야 할 것이 agent까지 흘러오는지 가시화.

Datadog 쿼리 예:

text
service:cupixworks-any-forge-agent status:error "handlingMessageErrors" -"PERM33000"
text
service:cupixworks-any-forge-agent "Errors in error handling" "PERM33000"

Risk Assessment#

  • Risk level: low — 사용자 영향은 제한적이며(빌링 만료 계정), 데이터 손상 없음. 운영상 로그 노이즈 및 forge_state 잔류가 주된 이슈.
  • 예상 복잡도: trivial (로그 레벨 조정) ~ standard (PERM-계열 선행 분기 및 단기 개선 포함).