ForgeService::handlingMessageErrors | Error and message object - {"error":{"message":"Request failed
RCA: ForgeService::handlingMessageErrors — Autodesk Model Derivative 500
Overview#
What Happened#
2026-07-29 02:35 KST 무렵 약 33초 동안 cupixworks-any-forge-agent가 4개의 서로 다른 BIM revision(25576, 25577, 25578, 25579)에 대해 Autodesk Model Derivative API(POST https://developer.api.autodesk.com/modelderivative/v2/designdata/job)에 translate job을 등록하려다 모두 HTTP 500(AxiosError / ERR_BAD_RESPONSE)을 받았다. 재시도 로직이 없어 ForgeService::handlingMessageErrors가 각 메시지마다 error 레벨 로그를 남겼고, SQS 메시지는 ApproximateReceiveCount=1 상태로 남아 이후 자동 재수신 시 재처리된다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | AxiosError |
| exception.message | Request failed with status code 500 |
| top_frame | axios@1.16.1/dist/node/axios.cjs:2069 (settle) |
| runtime | Node.js (node:events, node:internal/streams/readable) |
| downstream | POST https://developer.api.autodesk.com/modelderivative/v2/designdata/job (Autodesk Forge Model Derivative v2) |
| SDK | forge-apis/0.9.12 (nodejs) |
| env | production, us-west-2, tenant cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| moetest / Forge BIM translate 파이프라인 | 4 | 4개 BIM revision의 SVF2 + thumbnail 생성 job이 즉시 실패. SQS 재수신을 통한 자동 재시도로 최종 성공 여부는 확인 불가(추가 검증 필요). |
Timeline#
- 2026-07-29 02:35:23 KST — 최초 500 응답,
bim_revision 25576(SQS037a521b-...) - 2026-07-29 02:35:35 KST — 두 번째 500,
bim_revision 25578(SQS0b1c3ea0-...) - 2026-07-29 02:35:45 KST — 세 번째 500,
bim_revision 25579(SQSc5b1f150-...) - 2026-07-29 02:35:56 KST — 네 번째 500,
bim_revision 25577(SQS560f0b86-...) - 2026-07-29 02:36 KST 이후 — 동일 fingerprint 재발 없음 (7일 창 기준). Autodesk 측 일시적 5xx로 판단.
Error Log#
ForgeService::handlingMessageErrors | Error and message object - {"error":{"message":"Request failed with status code 500","name":"AxiosError","stack":"AxiosError: Request failed with status code 500
at settle (/tmp/agent/dist/node_modules/.pnpm/axios@1.16.1/node_modules/axios/dist/node/axios.cjs:2069:12)
at IncomingMessage.handleStreamEnd (/tmp/agent/dist/node_modules/.pnpm/axios@1.16.1/node_modules/axios/dist/node/axios.cjs:3713:11)
at IncomingMessage.emit (node:events:536:35)
at endReadableNT (node:internal/streams/readable:1698:12)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)","config":{...,"method":"post","url":"https://developer.api.autodesk.com/modelderivative/v2/designdata/job",...,"data":"{\"input\":{\"urn\":\"...\"},\"output\":{\"formats\":[{\"type\":\"svf2\",\"views\":[\"2d\",\"3d\"],\"advanced\":{\"conversionMethod\":\"v3\"}},{\"type\":\"thumbnail\",\"advanced\":{\"width\":400,\"height\":400}}]},\"misc\":{\"workflow\":\"25576bimrevision1785260115\"}}","allowAbsoluteUrls":true},"code":"ERR_BAD_RESPONSE","status":500},"sqsMessage":{"MessageId":"037a521b-42e4-4ca5-9bd8-d1853ddc8a2a","Attributes":{"ApproximateReceiveCount":"1"}}}
Impact#
- Service:
cupixworks-any-forge-agent - Team: moetest
- 발생 횟수: 4
- 최초 발생: 2026-07-29 02:35:23 KST
- 최근 발생: 2026-07-29 02:35:56 KST
Root Cause Summary#
Autodesk Model Derivative v2 API(POST /modelderivative/v2/designdata/job)가 33초 간 4건의 연속 요청에 대해 HTTP 500(ERR_BAD_RESPONSE)을 반환했다. 이는 4개의 서로 다른 BIM revision·URN·SQS 메시지에 걸쳐 발생했고 payload에는 클라이언트 오류가 없으므로, 근본 원인은 업스트림 Autodesk Forge 서비스의 일시적 5xx 이다. cupixworks-any-forge-agent는 이 호출을 감싸는 재시도(retry/backoff) 로직이 전혀 없어, 일시적 실패가 그대로 ForgeService::handlingMessageErrors까지 전파되어 error 로그를 남기고 SQS 메시지 삭제도 스킵된다.
Technical Analysis#
Code Path#
Entry point: applications/agents/packages/cupix-tesla-forge-agent/src/forge-service.ts:72 (checkingQueue 폴링 루프)
주요 실행 흐름:
checkingQueue→runByMessages(line 91)runByMessages→runByMessage(line 157)runByMessage→uploadBim(line 185)uploadBim→forgeApi.translate(line 218) — Failure point- axios rejection이 여기까지 던져지고,
checkingQueueline 92의 catch에서handlingMessageErrors호출 (line 93) handlingMessageErrors(line 390) 는 최종 error 로그를 line 412 에서 무조건 남김
forgeApi.translate 는 재시도 없이 SDK 호출만 한다:
translate = (encodedUrn: string, options?: any): Promise<FORGE.ApiResponse> => {
const jobPayload: any = {
input: { urn: encodedUrn },
output: { formats: [ /* svf2 + thumbnail */ ] },
misc: { workflow: options?.forgeWorkflowId != undefined ? options.forgeWorkflowId : undefined }
};
logger.debug('ForgeApi::translate | %s', JSON.stringify(jobPayload));
return this.derivativesApi.translate(
jobPayload,
{ xAdsForce: true },
this.forgeAuth.authClient, this.forgeAuth.authClient.getCredentials()
);
};
uploadBim 은 translate 실패 시 아무런 처리를 하지 않고 그대로 상위로 예외를 던진다:
if (forgeObjectId != undefined && cpBimRevision.forgeUrn) {
logger.info('ForgeService::runByMessage | forge urn: %s, workflow id: %s', cpBimRevision.forgeUrn, cpBimRevision.forgeWorkflowId);
await this.forgeApi.translate(cpBimRevision.forgeUrn, {
generateMasterViews: cpBim.isGenerateMasterViews,
forgeWorkflowId: cpBimRevision.forgeWorkflowId,
forgeOutputFormatType: forgeOutputFormatType
});
} else {
await this.updateBimRevisionForgeState(bimRevisionId, TESLA.UpdateBimRevisionRequest.ForgeStateEnum.Error);
}
checkingQueue catch 는 handlingMessageErrors 로 위임한다:
} else {
this._countWaitedToStopTask = 0;
await this.agentScaleOut();
try {
await this.runByMessages();
} catch (error) {
await this.handlingMessageErrors(error);
}
this.resetMessages();
await CPUtils.sleep(500);
await this.checkingQueue();
}
handlingMessageErrors 는 getApiErrorToDeleteMessage로 삭제 여부를 판정한 뒤, 원인·상태에 관계없이 line 412 에서 error 로그를 남긴다:
private handlingMessageErrors = async (error: any): Promise<void> => {
const errorAndMessage = {
error: error,
sqsMessage: {}
};
if (this.messageInProcess) {
errorAndMessage.sqsMessage = {
MessageId: this.messageInProcess.MessageId,
Attributes: this.messageInProcess.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();
};
getApiErrorToDeleteMessage 는 error.response.statusCode와 response.request.uri.href를 참조하지만, 이는 request 라이브러리 규약이지 axios 규약이 아니다. axios error 는 error.response.status(숫자, statusCode 아님)와 error.config.url 을 사용한다. 결과적으로 axios 4xx/5xx 오류에서 statusCode는 undefined가 되고 아래 분기는 통과 없이 return(undefined)한다:
private getApiErrorToDeleteMessage = (error: any): any => {
if (error == undefined) { ... return 'undefined error'; }
if (error.errno != undefined && error.code != undefined && error.syscall != undefined) { ... return; }
const response = CPUtils.isJsonString(error) ? JSON.parse(error) : error.response;
if (response == undefined) { ... return 'undefined response'; }
const statusCode = response.statusCode ? Number(response.statusCode) : undefined;
const requestUriHref = response.request && response.request.uri && response.request.uri.href;
// ...
logger.warn('ForgeService::getApiErrorToDeleteMessage | error msg - %s', JSON.stringify(errorMsg));
if (statusCode != undefined && statusCode >= 400 && statusCode <= 500) {
if (statusCode === 401) return;
return errorMsg;
}
return;
};
- 기대 동작: 업스트림 5xx 는 재시도로 흡수되거나, 적어도 warn 레벨로 로깅되어야 한다.
- 실제 동작: 재시도 없음 + 항상 error 레벨 로그 + axios 응답 필드 파싱 실패로 삭제 판정도 무의미(
ApproximateReceiveCount만으로 결정).
Log Evidence#
Datadog 쿼리 (재현 가능):
service:cupixworks-any-forge-agent status:error "ForgeService::handlingMessageErrors"
기간: now-2d (또는 클러스터 파일의 from_ts=1785256500000 / to_ts=1785263760000).
핵심 로그(각 이벤트 축약):
2026-07-29 02:35:23 KST error bim_revision=25576 workflow=25576bimrevision1785260115 sqs=037a521b-42e4-4ca5-9bd8-d1853ddc8a2a status=500
2026-07-29 02:35:35 KST error bim_revision=25578 workflow=25578bimrevision1785260127 sqs=0b1c3ea0-1100-4235-b141-c4de4423efdd status=500
2026-07-29 02:35:45 KST error bim_revision=25579 workflow=25579bimrevision1785260138 sqs=c5b1f150-8493-499e-b83b-a9bfc1b7a81c status=500
2026-07-29 02:35:56 KST error bim_revision=25577 workflow=25577bimrevision1785260149 sqs=560f0b86-6db8-4713-b051-4a5b7dbd3f2d status=500
각 이벤트는 ApproximateReceiveCount=1, 서로 다른 URN(tesla-production/{bimId}_...), 서로 다른 SQS MessageId. 즉 페이로드 개별 오류가 아니다.
동일 시각의 warn 로그(ForgeService::getApiErrorToDeleteMessage)는 axios 필드 미스매치로 인해 대부분 필드가 비어 있음을 보여준다:
{"modelId": 25576}
{"modelId": 25578}
{"modelId": 25579}
{"modelId": 25577}
즉 statusCode, requestUriHref, bodyMessage 가 모두 undefined로 직렬화되어 사라졌다.
Autodesk error payload 파싱 실패 근거:
- axios 응답은
error.response.status(number) 를 사용하지만 코드는response.statusCode를 읽음 → undefined. - axios 응답에는
response.request.uri.href가 없음. 대신error.config.url이 URL을 보유(로그 raw 에도config.url = https://developer.api.autodesk.com/modelderivative/v2/designdata/job확인됨).
Autodesk 측 응답 body 는 로그로 유실되었지만(response body 가 warn 로그에서 사라짐), code=ERR_BAD_RESPONSE, status=500 가 axios가 서버로부터 실제 5xx 를 받은 명확한 지표이다(네트워크 계층 오류였다면 code=ECONNREFUSED/ETIMEDOUT 등).
지난 7일간 동일 fingerprint (service:cupixworks-any-forge-agent "designdata/job") 검색에서 이 4건만 확인 → 국소적·일시적 사건.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Autodesk Model Derivative 서비스의 일시적 5xx (upstream 문제) | 4건 모두 서로 다른 bim_revision·URN·SQS 메시지에서 동일한 POST /modelderivative/v2/designdata/job 500 발생, 33초 창 안에서만 집중, 7일간 재발 없음, code=ERR_BAD_RESPONSE (axios가 서버 응답을 받았음을 의미) |
— | Confirmed |
| H2 | 클라이언트 페이로드 오류(400을 500으로 잘못 매핑 등) | — | 페이로드 스키마가 4건 모두 동일하고 표준(output.formats[].type: svf2 + thumbnail), 사전/사후 다른 시점의 동일 코드 경로는 정상 동작 (7일간 이 fingerprint 재발 0건) |
Rejected |
| H3 | 만료된 Autodesk OAuth 토큰으로 인한 실패 | — | 401이 아닌 500이며, x-ads-force: true와 함께 유효한 Bearer 토큰이 헤더에 포함됨. 401이었다면 getApiErrorToDeleteMessage가 별도 분기 처리(line 374) |
Rejected |
| H4 | 네트워크 계층/연결 실패(DNS, connection reset 등) | — | axios가 ERR_BAD_RESPONSE(HTTP 응답 수신 후 파싱 단계)로 종료. 네트워크 오류였다면 ECONNRESET/ETIMEDOUT 등 error.errno/code/syscall 트리플이 존재해야 하고 getApiErrorToDeleteMessage의 line 347 분기로 빠졌을 것 |
Rejected |
| H5 | Autodesk rate limit (429가 500으로 노출) | — | 33초에 4건은 rate limit을 유발할 부하가 아님. 또한 x-ads-force: true 헤더는 rate limit 우회 목적이 아니라 재변환 강제 옵션 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 이번 사건은 업스트림 Autodesk 서비스의 일시적 이슈로 판단되며, 재발 시점 기준 7일간 동일 fingerprint 로 재현되지 않았다. 코드 변경 없이 관찰만 해도 무방하지만, 재발 방지를 위한 개선 여지가 명확하다. 아래는 후속 개선안(즉시 롤아웃 불필요).
- 우선 확인해야 할 운영 사항:
bim_revision 25576, 25577, 25578, 25579의 SQS 재수신을 통한 재처리 성공 여부(SQS visibility timeout 이후 자동 재처리) — Datadog에서 해당 revision id 로 후속ForgeService::runByMessage | forge object id: ...성공 로그가 남았는지 확인 필요(uncertain — needs verification).
단기 개선 (1주 이내)#
- 재시도 계층 추가:
applications/agents/packages/api/src/forge-api.ts:319translate(및 유사한getManifest,downloadObject등 Forge SDK 호출) 를 exponential backoff 재시도로 감싼다. 참고 패턴: 같은 monorepopackages/api/src/authentication/cupix-auth.ts의retryable헬퍼(memory 기록:MaxRetries=3,e.statusCode > 500기준). Forge SDK 는 axios 기반이므로 조건을e.response?.status >= 500 && e.response?.status < 600로 맞춰야 한다(statusCode아님). - 로그 레벨 다운그레이드:
ForgeService::handlingMessageErrors에서 최종 error 로그(line 412)를, 재시도가 흡수 가능한 5xx 인 경우 warn 으로 분기. 형식 문자열ForgeService::handlingMessageErrors | Error and message object - %s는 Datadog fingerprint 이므로 warn/error 두 분기 모두 동일하게 유지(potree-agent 에피소드 cf86fc53 와 동일 패턴). - axios 응답 파싱 수정:
getApiErrorToDeleteMessage(line 342) 는response.statusCode/response.request.uri.href를 사용해 axios 오류를 정확히 분류하지 못한다. axios 인 경우error.response?.status,error.config?.url,error.response?.data로 매핑해야 warn 로그가 실제 진단 정보(status, URL, Autodesk body)를 포함하게 된다. 사전에 팀 로거 컨벤션(memory: “agents 코드베이스는 fix logger 가 Error 를 그대로 처리”) 확인 후 반영.
장기 개선 (재발 방지)#
- Forge SDK 를 감싸는 얇은 클라이언트 레이어에 서킷 브레이커 + 관측(요청 성공률, 5xx 비율) 을 도입하고, 실패한 revision 은 dead-letter 처리 또는 backoff 후 재시도 큐로 이동. 현재는 SQS visibility timeout에 의존한 자연 재시도만 존재하며
ApproximateReceiveCount ≥ MaxReceiveCount도달 시 최종 실패 처리(updateBimForgeState(Error))가 된다. - Autodesk Forge Model Derivative 의 정기 상태 페이지(https://health.autodesk.com/) 를 status-board 데이터 소스에 추가해 upstream 이벤트를 자동 인식.
Monitoring#
Datadog release dashboard timeseries widget 용 쿼리(모두 timeseries 검증 문법 준수):
ForgeService translate 실패 카운트(재발 감지):
service:cupixworks-any-forge-agent status:error "ForgeService::handlingMessageErrors" "designdata/job"
Autodesk 5xx 응답률(총 요청 대비):
service:cupixworks-any-forge-agent "designdata/job" "\"status\":500"
SQS 처리 시 receive count 상위(재시도 소진 임박):
service:cupixworks-any-forge-agent "ForgeService::addMessagesToList" @receive_count:>=3
권장 알림: 10분 창 내 ForgeService::handlingMessageErrors 이벤트 ≥5건 이면 Autodesk 업스트림 이슈로 태그.
Risk Assessment#
- Risk level: low (33초 짜리 일시적 upstream 이슈, 7일간 재발 없음, SQS 재수신으로 자연 회복 가능)
- 예상 복잡도: standard (재시도 랩퍼 + 로그 레벨 분기 + axios 응답 필드 매핑 수정. potree-agent 에피소드 cf86fc53 의 유사 패턴 재사용 가능)