ES /docs

ForgeApi::uploadStream | end, response error: {"statusCode":503,"headers":{"date":"Tue, 07 Jul 26 16

RCA: ForgeApi::uploadStream 503 from Autodesk OSS direct-upload S3

Overview#

What Happened#

2026-07-08 01:04 KST 에 cupixworks-any-forge-agent (production, us-west-2) 의 ForgeApi::uploadStream 이 Autodesk Forge OSS 의 pre-signed S3 direct-upload 엔드포인트 (com-autodesk-oss-direct-upload.s3.amazonaws.com) 로 5MB 파트 PUT 을 보내는 중 HTTP 503 응답을 받고 에러 로그를 남겼다. 14일 retention 창 안에서 이 fingerprint 로 관측된 이벤트는 1건뿐이며, 관련 dep 인시던트도 열려 있지 않다.

Quick Facts#

Field Value
exception.class n/a (logger.error only, promise reject() without value)
exception.message `ForgeApi::uploadStream
top_frame packages/api/src/forge-api.ts:125
runtime Node.js agent (cupixworks/applications/agents)
env production, us-west-2
upstream com-autodesk-oss-direct-upload.s3.amazonaws.com (AmazonS3, x-amz-request-id 27FCA8130B3AB887)

Affected Teams#

Team / Domain Error Count Impact
redwoodeg / forge-agent BIM 업로드 1 단일 BIM revision 업로드 파트가 실패, 상위 로직에서 재시도 없이 실패 전파 (uncertain — 실제 SQS 재시도 여부는 message-level 로그에서 미확인)

Timeline#

  1. 2026-07-08 01:04:45 KST — S3 서명 URL 발급 (X-Amz-Date=20260707T160445Z, 유효기간 899s).
  2. 2026-07-08 01:04:46 KSTPUT partNumber=1&uploadId=DRbTdqpActoutWMg... 요청 → S3 응답 503, content-length: 0, connection: close, server: AmazonS3 (x-amz-request-id: 27FCA8130B3AB887).
  3. 2026-07-08 01:04:46 KSTforge-api.ts:125 에서 error 로그 기록 후 promise reject.

Error Log#

Datadog Logs

text
ForgeApi::uploadStream | end, response error: {"statusCode":503,"headers":{"date":"Tue, 07 Jul 26 16:04:46 GMT","connection":"close","x-amz-id-2":"D5zLSDbvKSsbWLt1OFLbWa+VoSl3fK9iFe92eJe0kOqx/PWOQclLOB0GcgggsoBFy/gg/URoyY7xArOGfNk33fK0WfDMf5tO4knFJCDPRnyjkohMmlAPIA==","x-amz-request-id":"27FCA8130B3AB887","content-type":"application/xml","server":"AmazonS3","content-length":"0"},"request":{"uri":{"host":"com-autodesk-oss-direct-upload.s3.amazonaws.com","pathname":"/89/de/2e/d5/bucket/tesla-production/signed-url-uploads/89de2ed5-04ef-4e93-9b86-a91a7f80bba7"},"method":"PUT","headers":{"Content-Type":"application/octet-stream","Content-Length":5242880}}}

Impact#

  • Service: cupixworks-any-forge-agent
  • Team: redwoodeg
  • 발생 횟수: 1
  • 최초 발생: 2026-07-08 01:04 KST
  • 최근 발생: 2026-07-08 01:04 KST

Root Cause Summary#

Autodesk Forge OSS 가 반환하는 pre-signed URL 이 가리키는 외부 S3 버킷 (com-autodesk-oss-direct-upload.s3.amazonaws.com, us-east-1) 에서 PUT part 요청에 대해 일시적으로 503 을 반환했다. 이 버킷은 Cupix 소유가 아니라 Autodesk Forge Model Derivative 서비스가 signed direct-upload target 으로 제공하는 것이며, S3 는 스루풋 급증 또는 파티션 튜닝 중 503 Slow Down 을 낼 수 있다. ForgeApi.uploadStream 은 응답 statusCode 가 200 이 아니면 즉시 reject() (값 없음) 로 실패시키며 재시도 로직이 없기 때문에, 단발 503 이 곧바로 상위 워크플로 실패로 이어졌다. 14일 창에서 동일 fingerprint 이벤트가 1건, 다른 uploadStream 에러는 0건이므로 외부 서비스의 단발 transient 장애가 root cause 다.

Technical Analysis#

Code Path#

  • Entry (caller): packages/cupix-tesla-forge-agent/src/forge-service.ts:210forgeApi.uploadFileSignedS3(cpBim.originalFilePath, cpBim.objectName)
  • Orchestrator: packages/api/src/forge-api.ts:160-234uploadFileSignedS3 가 파일을 5MB 청크로 나누고 getS3SignedUrl 로 Autodesk OSS 에서 파트별 signed URL 을 받은 뒤 (urls[chunkIdx]) uploadStream 을 최대 5개 동시 실행.
  • Failure point: packages/api/src/forge-api.ts:113-132uploadStreamrequest.put(signedUrl, ...) 응답을 받아 statusCode 가 200 이 아니면 error 로그 후 reject().

uploadStream 구현:

packages/api/src/forge-api.ts:113-132typescript
private uploadStream = (signedUrl: string, stream: fs.ReadStream, length: number): Promise<any> => new Promise((resolve, reject) => {
    stream.pipe(request.put(signedUrl, {
        headers: {
            'Content-Type': 'application/octet-stream',
            'Content-Length': length
        }
    }).on('response', (res) => {
        const data: any = {};
        if (res.statusCode === 200) {
            data.eTag = res.headers.etag?.replace(/"/g, '');
            resolve(data);
        } else {
            logger.error('ForgeApi::uploadStream | end, response error: %s', JSON.stringify(res));
            reject();
        }
    })).on('error', err => {
        logger.error('ForgeApi::uploadStream | end, error: %s', JSON.stringify(err));
        reject(err);
    });
});

uploadFileSignedS3 청크 병렬 실행 및 실패 처리:

packages/api/src/forge-api.ts:197-223typescript
await new Promise((resolve, reject) => {
    eachLimit(
        uploadTasks,
        this.CONCURRENT_UPLOADS,
        (task, callback) => {
            logger.debug('ForgeApi::uploadFileSignedS3 | begin chunk index: %d', task.chunkIndex);
            return task.run()
                .then(data => {
                    // ...
                    return callback();
                }, err => {
                    return callback(err);
                });
        },
        err => {
            if (err) {
                logger.warn('ForgeApi::uploadFileSignedS3 | %s', JSON.stringify(err));
                reject(err);
            }
        });
});

기대 동작: pre-signed PUT 이 200 이면 eTag 를 모아 completeS3Upload 로 파트를 조립. transient 5xx 에 대해서는 재시도 필요.

실제 동작: Autodesk OSS 가 리다이렉트한 실제 S3 origin (server: AmazonS3, content-length: 0 — 전형적인 S3 503 SlowDown/InternalError 시그니처) 이 첫 번째 파트에 503 을 반환. uploadStream 이 값 없이 reject()eachLimit 이 그대로 상위 promise 를 reject → uploadFileSignedS3 는 warn 로그 (ForgeApi::uploadFileSignedS3 | ...) 하나 남기고 caller 에게 throw. 재시도가 없어 단발 503 이 곧 파일 업로드 실패로 이어짐.

부수 결함: reject() 가 인자 없이 호출되어 caller 는 undefined 를 err 로 받는다 (line 126). 실패 원인 (503 응답) 이 상위 로직으로 전달되지 않는다.

Log Evidence#

Datadog 쿼리 (14일 창, service 필터):

text
service:cupixworks-any-forge-agent status:error "ForgeApi::uploadStream"

결과: 총 1건 (2026-07-08 01:04:46 KST). 상세는 위 "Error Log" 참조.

동일 서비스에서 관련 후속 로그 확인:

text
service:cupixworks-any-forge-agent "uploadFileSignedS3"

결과: 0건 (최근 24h). uploadFileSignedS3 는 실패 시 logger.warn 을 남기지만 (line 219), 이 시점 warn 이 Datadog 에 저장돼 있지 않아 상위 흐름 (재시도/DLQ 이동 여부) 은 로그로 재구성 불가 — uncertain, needs verification.

S3 응답 헤더 상 signature:

json
{
  "statusCode": 503,
  "server": "AmazonS3",
  "content-length": "0",
  "connection": "close",
  "x-amz-id-2": "D5zLSDbvKSsbWLt1OFLbWa+VoSl3fK9iFe92eJe0kOqx/PWOQclLOB0GcgggsoBFy/gg/URoyY7xArOGfNk33fK0WfDMf5tO4knFJCDPRnyjkohMmlAPIA==",
  "x-amz-request-id": "27FCA8130B3AB887"
}

content-length: 0 + content-type: application/xml 조합은 S3 가 SlowDown/InternalError 응답 바디를 잘라 보낸 경우에 해당한다 (본문 XML 이 유실됐지만 status 만 반환). AWS 는 이 상황을 재시도 대상 5xx 로 분류한다.

Status board 조회:

text
bun run cli/incident-board.ts for-cluster c33602ef-d174-4180-a0e9-d80f8da4eb5e
→ { "scope": "svc:cupixworks-any-forge-agent::unknown", "active": null, "recent": [] }

동일 스코프의 활성/최근 dep 인시던트 없음 → 광역 S3 장애가 아니라 단일 요청 transient.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Autodesk OSS 가 리다이렉트한 S3 target 의 transient 5xx (SlowDown/InternalError) 응답 503 + server:AmazonS3 + content-length:0 + connection:close; 14일 창 유일 1건; status board active dep 인시던트 없음 Confirmed
H2 pre-signed URL 만료로 인한 실패 signed URL 발급 시각 X-Amz-Date=20260707T160445Z, 실패 시각 16:04:46Z — 1초 이내이며 X-Amz-Expires=899 (15분) 로 만료 아님. 만료라면 S3 는 403 을 반환 (503 아님). 403 이 아니라 503 이 반환됨 Rejected
H3 Cupix 소유 버킷 (tesla-production) 자체의 문제 로그의 pathname 에 tesla-production 문자열 포함 실제 host 는 com-autodesk-oss-direct-upload.s3.amazonaws.com (Autodesk 소유 S3). pathname 은 Autodesk 가 사용하는 내부 키 구조일 뿐이며 Cupix 는 이 버킷에 직접 접근하지 않는다. forge-api.ts:40 의 Cupix bucket key 는 별개. Rejected
H4 uploadStream 내부 로직 결함이 root cause reject() 가 인자 없이 호출되어 caller 가 원인을 잃는다 (line 126); 재시도 부재 이는 관측성/견고성 결함이지 이번 503 자체의 원인이 아님. 원인은 외부 S3 응답. Rejected as root cause (retained as contributing factor)

Fix Recommendation#

즉시 조치 (Critical)#

없음. 단발 transient 외부 5xx 로, 즉시 코드 변경 없이 관망 가능. dep 인시던트도 열려 있지 않다. 상위 워크플로가 재처리에 실패해 사용자 impact 가 확인되면 그때 아래 단기 개선 항목을 앞당긴다.

단기 개선 (1주 이내)#

  • 대상 파일: packages/api/src/forge-api.ts:113-132 (uploadStream) 및 packages/api/src/forge-api.ts:197-223 (uploadFileSignedS3 의 청크 실행부).
  • 방향:
    • uploadStream 에서 5xx (특히 500/502/503/504) 및 네트워크 error 를 구분해 지수 백오프로 파트 단위 재시도 (예: 3회, 1s/2s/4s + jitter). AWS SDK 및 Autodesk 문서 모두 pre-signed direct-upload S3 5xx 를 재시도 대상으로 규정.
    • reject() 호출을 reject(new Error(...)) 로 바꿔 statusCode, x-amz-request-id, uploadKey, partNumber 를 캐리. 상위 uploadFileSignedS3 의 warn 로그가 원인 없이 undefined 만 남는 현상 제거.
  • 근거: 이번 이벤트가 재시도되었다면 사용자 impact 없이 해소됐을 확률이 높다 (SlowDown 은 통상 수 초 안에 회복). 재시도 부재가 단발 5xx 를 곧바로 워크플로 실패로 확대한다.

장기 개선 (재발 방지)#

  • Autodesk Forge 업로드 실패에 대한 SQS/DLQ 재시도 정책을 문서화 및 검증. ForgeService::handlingMessageErrors 경로에서 5xx upstream 실패가 어떻게 처리되는지 (재시도 vs DLQ 이동) 명확히 로그로 남길 것 — 현재는 uploadFileSignedS3 실패 이후 흔적이 로그에 없다 (uncertain).
  • request (deprecated) 대신 axios/undici + built-in retry 어댑터로 마이그레이션 검토. 이미 같은 파일 내에서 axios 를 다른 경로 (downloadObject) 에 사용 중이므로 라이브러리 통일이 자연스럽다.

Monitoring#

  • Autodesk OSS direct-upload S3 5xx 발생률을 별도 위젯으로 추적.

Datadog timeseries 쿼리 (release dashboard widget 용):

text
logs("service:cupixworks-any-forge-agent status:error \"ForgeApi::uploadStream\" \"statusCode\\\":503\"").index("*").rollup("count").by("environment")
text
logs("service:cupixworks-any-forge-agent status:error \"ForgeApi::uploadStream\"").index("*").rollup("count").by("environment")
  • 알림 임계값 (참고): 10분 창에서 3건 이상 시 warn, 10건 이상 시 alert. 단발 SlowDown 은 무시하되 클러스터화된 실패는 즉시 감지.

Risk Assessment#

  • Risk level: low (14일 창 1건, dep 인시던트 없음, external transient)
  • 예상 복잡도: standard (재시도 + reject 인자 개선은 국소 변경, 테스트 필요)