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#
- 2026-05-12 06:15:52 UTC — bim id 220, bim_revision 253에 대해
runByMessages중PUT /api/v1/bim_revisions/253→ 403 PERM33000 - 2026-05-12 06:15:52 UTC —
handlingMessageErrors가updateBimForgeState(220, Error)시도 → 동일한 403 PERM33000 → "Errors in error handling" 로그 - 2026-05-12 06:15:56 UTC — bim id 219, bim_revision 252에 대해 동일 시나리오 재현
Error Log#
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_state를 Error로 기록하지 못하므로, 빌링 만료 테넌트의 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는 실패한 메시지의 상태를 반영하기 위해 updateBimForgeState → PUT /api/v1/bims/{id}를 호출하는데, 같은 빌링 만료 조건 때문에 이 상태 업데이트 호출 또한 동일하게 PERM33000을 반환한다. 에러 핸들러의 중첩 try/catch가 이 2차 예외를 잡아 "Errors in error handling" 로그로 기록한다. 즉, 이 로그는 새로운 결함이 아니라 빌링 만료 테넌트에서 예측 가능한 운영 시나리오가 error 레벨로 노출된 것이다.
Technical Analysis#
Code Path#
- Entry point:
forge-service.ts:91—await this.runByMessages() - Error origin:
forge-service.ts:214or:276—cupixApi.bimRevision.update(bimRevisionId, ...)returns 403 - Error handler entry:
forge-service.ts:93—await this.handlingMessageErrors(error) - Failure point (inner):
forge-service.ts:405—updateBimForgeState(bimId, Error)callsPUT /api/v1/bims/{id}which also returns 403 - Logged at:
forge-service.ts:407—logger.error('ForgeService::handlingMessageErrors | Errors in error handling - %s', JSON.stringify(error)) - Upstream validator:
tesla/lib/cupix/abstract/base.rb:38
try {
await this.runByMessages();
} catch (error) {
await this.handlingMessageErrors(error);
}
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();
};
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');
};
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_state를 Error로 반영할 수 없고 운영 대시보드에서는 에러로 노출됨.
Log Evidence#
Datadog query:
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"). 핵심 원문:
{
"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" } }
}
즉, 원본 실패 URL은 PUT /api/v1/bim_revisions/253 (bimRevision 업데이트 단계), 이어지는 "Errors in error handling"의 실패 URL은 PUT /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 분기에서 updateBimForgeState → PUT /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.codeprefixPERM) 에 한해logger.error를logger.warn으로 낮추거나, 내부try에서 별도로 인지/기록 후 정상 흐름으로 종료. 동시에"Errors in error handling"로그 메시지는 진짜 예외(네트워크/5xx)에만 남도록 분기. - 근거: MEMORY.md 기록 — 빌링 만료, cross-region, 레이트 리밋 등 예측 가능한 운영 시나리오는
warn이 적절. 현재 구현은 의도된 거절까지error로 노출되어 알람 노이즈.
단기 개선 (1주 이내)#
getApiErrorToDeleteMessage(forge-service.ts:342-378)에서 401 외에도 의도된 정책 위반(PERM33000 등) 을 식별하여,handlingMessageErrors의updateBimForgeState호출을 스킵하도록 분기. 빌링 만료 상태에서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 쿼리 예:
service:cupixworks-any-forge-agent status:error "handlingMessageErrors" -"PERM33000"
service:cupixworks-any-forge-agent "Errors in error handling" "PERM33000"
Risk Assessment#
- Risk level: low — 사용자 영향은 제한적이며(빌링 만료 계정), 데이터 손상 없음. 운영상 로그 노이즈 및
forge_state잔류가 주된 이슈. - 예상 복잡도: trivial (로그 레벨 조정) ~ standard (PERM-계열 선행 분기 및 단기 개선 포함).