ES /docs

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#

  1. 2026-07-29 02:35:23 KST — 최초 500 응답, bim_revision 25576 (SQS 037a521b-...)
  2. 2026-07-29 02:35:35 KST — 두 번째 500, bim_revision 25578 (SQS 0b1c3ea0-...)
  3. 2026-07-29 02:35:45 KST — 세 번째 500, bim_revision 25579 (SQS c5b1f150-...)
  4. 2026-07-29 02:35:56 KST — 네 번째 500, bim_revision 25577 (SQS 560f0b86-...)
  5. 2026-07-29 02:36 KST 이후 — 동일 fingerprint 재발 없음 (7일 창 기준). Autodesk 측 일시적 5xx로 판단.

Error Log#

Datadog Logs

text
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 폴링 루프)

주요 실행 흐름:

  1. checkingQueuerunByMessages (line 91)
  2. runByMessagesrunByMessage (line 157)
  3. runByMessageuploadBim (line 185)
  4. uploadBimforgeApi.translate (line 218) — Failure point
  5. axios rejection이 여기까지 던져지고, checkingQueue line 92의 catch에서 handlingMessageErrors 호출 (line 93)
  6. handlingMessageErrors (line 390) 는 최종 error 로그를 line 412 에서 무조건 남김

forgeApi.translate 는 재시도 없이 SDK 호출만 한다:

applications/agents/packages/api/src/forge-api.ts:319-358typescript
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()
    );
};

uploadBimtranslate 실패 시 아무런 처리를 하지 않고 그대로 상위로 예외를 던진다:

applications/agents/packages/cupix-tesla-forge-agent/src/forge-service.ts:216-225typescript
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 로 위임한다:

applications/agents/packages/cupix-tesla-forge-agent/src/forge-service.ts:87-98typescript
} 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();
}

handlingMessageErrorsgetApiErrorToDeleteMessage로 삭제 여부를 판정한 뒤, 원인·상태에 관계없이 line 412 에서 error 로그를 남긴다:

applications/agents/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: 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();
};

getApiErrorToDeleteMessageerror.response.statusCoderesponse.request.uri.href를 참조하지만, 이는 request 라이브러리 규약이지 axios 규약이 아니다. axios error 는 error.response.status(숫자, statusCode 아님)와 error.config.url 을 사용한다. 결과적으로 axios 4xx/5xx 오류에서 statusCodeundefined가 되고 아래 분기는 통과 없이 return(undefined)한다:

applications/agents/packages/cupix-tesla-forge-agent/src/forge-service.ts:342-378typescript
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 쿼리 (재현 가능):

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

기간: now-2d (또는 클러스터 파일의 from_ts=1785256500000 / to_ts=1785263760000).

핵심 로그(각 이벤트 축약):

text
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 필드 미스매치로 인해 대부분 필드가 비어 있음을 보여준다:

json
{"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/ETIMEDOUTerror.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:319 translate (및 유사한 getManifest, downloadObject 등 Forge SDK 호출) 를 exponential backoff 재시도로 감싼다. 참고 패턴: 같은 monorepo packages/api/src/authentication/cupix-auth.tsretryable 헬퍼(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 실패 카운트(재발 감지):

text
service:cupixworks-any-forge-agent status:error "ForgeService::handlingMessageErrors" "designdata/job"

Autodesk 5xx 응답률(총 요청 대비):

text
service:cupixworks-any-forge-agent "designdata/job" "\"status\":500"

SQS 처리 시 receive count 상위(재시도 소진 임박):

text
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 의 유사 패턴 재사용 가능)