AwsS3Manager::uploadDirectoryByCredential | upload failed - {"$fault":"server","$metadata":{"httpSta
RCA: AwsS3Manager::uploadDirectoryByCredential upload failed (S3 InternalError 500)
Overview#
What Happened#
2026-08-05 17:14 KST, pano postprocessing agent(cupixworks-pano-postprocessor-instance)이 리사이즈된 pano 타일을 S3에 업로드하다가 S3가 InternalError (HTTP 500, We encountered an internal error. Please try again.)를 반환했다. AWS SDK가 3회 재시도한 뒤에도 실패했고, 이 에러는 파일 단위 catch 블록에서 로그만 남긴 채 삼켜졌다. 단발성 발생(1건)이며, 같은 fingerprint에 묶인 유사 transport 실패가 14일간 총 6건 있었다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | InternalError (S3 server fault) |
| exception.message | We encountered an internal error. Please try again. |
| top_frame | applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:139 |
| runtime | Node.js agent, aws-sdk S3 client s3.upload().promise() |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| gad (pano postprocessor) | 1 | 리사이즈 타일 1개 이상이 S3에 업로드되지 못했으나, 에러가 삼켜져 pano는 정상 처리로 표시될 수 있음 |
Timeline#
- 2026-08-05 17:14:19 KST —
AwsS3Manager::uploadDirectoryByCredential에서 S3InternalError500 발생, SDK 3회 재시도 후 실패,logger.error로 기록 (first_seen = last_seen). - 2026-08-05 (RCA) — error-sweeper가 클러스터 감지, RCA 수행.
Error Log#
AwsS3Manager::uploadDirectoryByCredential | upload failed - {"$fault":"server","$metadata":{"httpStatusCode":500,"requestId":"9C6GEPQYAQ5X0AY5","extendedRequestId":"VKULUAs/N3p2eWYqiw6Bx+YGppfEg7hqHaIzxDtKHOjHn3/Xf4urZNeUUWwyttU1eq0PxpgN+1o=","attempts":3,"totalRetryDelay":95},"name":"InternalError","Code":"InternalError","RequestId":"9C6GEPQYAQ5X0AY5","HostId":"VKULUAs/N3p2eWYqiw6Bx+YGppfEg7hqHaIzxDtKHOjHn3/Xf4urZNeUUWwyttU1eq0PxpgN+1o=","message":"We encountered an internal error. Please try again."}
Impact#
- Service:
cupixworks-pano-postprocessor-instance - Team: gad
- 발생 횟수: 1
- 최초 발생: 2026-08-05 17:14 KST
- 최근 발생: 2026-08-05 17:14 KST
Root Cause Summary#
S3가 반환한 InternalError (HTTP 500)는 AWS 측 서버의 일시적 내부 오류다. $fault: "server" 및 "attempts": 3이 이를 확증한다. 즉 AWS SDK가 이미 자체 재시도(3회)를 소진한 뒤에도 S3 서버가 계속 500을 반환한 transient 이벤트로, pano postprocessor 코드의 결함은 아니다. 다만 코드 측 resilience gap이 하나 있다. uploadDirectoryByCredential의 파일 단위 catch 블록(aws-s3.manager.ts:138-140)이 실패를 로그만 남기고 re-throw하지 않아서, 개별 파일 업로드가 실패해도 함수는 정상 resolve된다. 그 결과 상위 호출부 uploadResizedImage(resize-work.ts:141-142)가 곧바로 checkTileUploading을 호출해 타일 업로드를 완료로 표시하고, pano-postprocessor-service.ts:171의 상위 catch도 트리거되지 않아 pano가 Error 상태로 기록되지 않는다. 근본 트리거는 외부 의존성(S3)의 일시 장애이나, 이 삼킴 로직이 부분 실패를 조용히 감춘다.
Technical Analysis#
Code Path#
- Entry point:
applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:165-178(resize task 루프) resize-work.ts:138-143uploadResizedImage가 credential을 발급받아 디렉터리를 업로드하고 타일 업로드 완료를 통지- Failure point:
aws-s3.manager.ts:130-140s3.upload().promise()가 S3InternalError500으로 실패,catch에서 로그만 남김
resize task는 세 단계를 순차 실행한다. 업로드는 uploadResizedImage 안에서 일어난다.
const resizeTasks = cpPanos.map((cpPano) => {
return PARALLEL_TASK_LIMIT(async () => {
try {
await this.resizeWork.resizePano(cpPano);
await this.resizeWork.uploadResizedImage(cpPano);
await this.resizeWork.checkStitched(cpPano);
} catch (error) {
logger.error('PanoPostprocessorService::run | resize pano id:%d | error %s', cpPano.panoId, stringifyError(error));
await this.panoPostprocessorManager.updatePanoState(cpPano.panoId!, TESLA.UpdatePanoRequest.StateEnum.Error);
erroredPanoIds.add(cpPano.panoId!);
}
});
});
uploadResizedImage는 credential 발급 → 디렉터리 업로드 → 타일 업로드 완료 통지 순으로 진행한다.
uploadResizedImage = async (cpPano: CPPano): Promise<void> => {
if (!cpPano.panoId) return;
const credentials = await this.cupixApi.pano.createTileUploadCredentials(cpPano.panoId);
await this.awsS3Manager.uploadDirectoryByCredential({ credential: credentials, directory: cpPano.resizeDir!});
await this.cupixApi.pano.checkTileUploading(cpPano.panoId);
};
실제 업로드는 파일 단위 Promise.all 청크로 수행되며, 여기서 S3 InternalError가 발생한다. catch는 로그만 남기고 예외를 다시 던지지 않는다.
await s3.upload({
Bucket: credential.bucket_name!,
Key: bucketKey,
Body: fs.createReadStream(filePath)
}).promise();
logger.debug('AwsS3Manager::uploadDirectoryByCredential | uploaded file - %s', filePath);
uploadedFileCount++;
} catch (error) {
logger.error('AwsS3Manager::uploadDirectoryByCredential | upload failed - %s', JSON.stringify(error));
}
기대 동작: 타일 파일 업로드가 실패하면 pano를 Error 상태로 표시하거나 최소한 부분 실패를 상위로 전파해야 한다.
실제 동작: catch가 예외를 삼켜 uploadDirectoryByCredential가 정상 resolve → uploadResizedImage가 checkTileUploading으로 완료 통지 → pano-postprocessor-service.ts:171의 상위 catch 미발동 → pano가 성공으로 간주됨. uploadedFileCount는 실패 파일만큼 증가하지 않지만 이 값을 검증하는 로직이 없다.
Log Evidence#
Datadog 쿼리 (재현):
service:cupixworks-pano-postprocessor-instance status:error "AwsS3Manager::uploadDirectoryByCredential"
이 클러스터의 대표 로그 (2026-08-05 17:14:19 KST). $fault: "server"와 attempts: 3이 S3 서버 측 일시 장애 + SDK 재시도 소진을 확증한다.
{
"timestamp": "2026-08-05 17:14:19",
"status": "error",
"message": "AwsS3Manager::uploadDirectoryByCredential | upload failed - {\"$fault\":\"server\",\"$metadata\":{\"httpStatusCode\":500,\"requestId\":\"9C6GEPQYAQ5X0AY5\",\"attempts\":3,\"totalRetryDelay\":95},\"name\":\"InternalError\",\"Code\":\"InternalError\",\"message\":\"We encountered an internal error. Please try again.\"}"
}
같은 fingerprint(AwsS3Manager::uploadDirectoryByCredential | upload failed prefix로 그룹화)에 묶인 14일간 6건은 서로 다른 3종의 transport 실패다. 모두 attempts: 3으로 SDK 재시도를 소진한 후 표면화됐다. 이는 특정 코드 경로 결함이 아니라 S3/네트워크 transport의 일시 불안정을 가리킨다.
2026-08-05 17:14:19 InternalError httpStatusCode:500 attempts:3 (server fault)
2026-08-05 01:11:38 TimeoutError code:ECONNRESET attempts:3 (transport reset)
2026-08-05 01:11:38 TimeoutError code:ECONNRESET attempts:3 (transport reset)
2026-07-29 01:40:26 Unknown httpStatusCode:499 attempts:1 (client close)
2026-07-29 01:40:08 Unknown httpStatusCode:499 attempts:1 (client close)
2026-07-29 01:40:02 Unknown httpStatusCode:499 attempts:1 (client close)
status-board 조회 결과 이 scope에 진행 중이거나 최근 해소된 인시던트는 없다.
{ "scope": "svc:cupixworks-pano-postprocessor-instance::unknown", "active": null, "recent": [] }
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | S3 서버 측 일시 장애(InternalError 500)가 트리거이고, 코드는 파일 단위 실패를 삼켜 부분 실패를 은폐 |
로그의 $fault:"server", httpStatusCode:500, attempts:3, message We encountered an internal error. Please try again.; catch가 re-throw 없이 로그만 남김 (aws-s3.manager.ts:138-140) |
— | Confirmed |
| H2 | pano postprocessor 코드 로직 결함(잘못된 key/bucket/credential)으로 인한 결정론적 실패 | — | 에러가 $fault:"server" 500이고 credential/bucket 구성 오류라면 4xx(403/400)로 반환됨. 실제 별건 credential 에러는 별도 fingerprint(403 HttpError on createTileUploadCredentials, 2026-08-04)로 분리됨 |
Rejected |
| H3 | 특정 pano/파일에 대한 반복 재진입 실패(broad 회귀) | 14일 6건 | 6건이 3종 transport 에러(500/ECONNRESET/499)로 분산, 서로 다른 requestId, 저빈도 산발 → 일시적 transport 이벤트 | Rejected |
| H4 | SDK 재시도 미설정으로 인한 즉시 실패 | — | 로그의 attempts:3이 SDK 기본 재시도(3회)가 이미 동작했음을 확증; 재시도 소진 후에도 S3가 계속 500 반환 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
코드 변경 불필요. 근본 트리거는 S3의 일시적 InternalError 500이며, AWS SDK가 이미 3회 재시도를 수행했다. 단발 transient 이벤트로, 해당 pano의 재처리(재큐잉)로 자연 복구된다.
단기 개선 (1주 이내)#
aws-s3.manager.ts:138-140의 파일 단위catch가 실패를 삼키는 문제를 개선한다. 실패한 파일을 집계해 청크 종료 후 하나라도 실패했으면uploadDirectoryByCredential가 예외를 re-throw하도록 방향을 잡는다. 그러면uploadResizedImage가checkTileUploading으로 완료 통지하기 전에 상위pano-postprocessor-service.ts:171catch가 pano를Error상태로 표시할 수 있다. 이는 부분 업로드 실패로 타일이 누락된 pano가 조용히 성공으로 기록되는 것을 막는다.- 로그 직렬화 개선:
catch가JSON.stringify(error)를 사용하는데,Error의 non-enumerable 속성이 손실될 수 있다. 이 패키지의cploggererrorSafeFormat규약(memory: pano-postprocessor 서비스 노트)에 맞춰error를 splat 인자로 직접 전달하는 방향을 검토한다. 이번 사례는 SDK가 plain object 형태의 에러를 던져 stringify가 동작했지만, 다른 경로에서는{}로 직렬화될 위험이 있다.
장기 개선 (재발 방지)#
- 타일 업로드의 무결성 검증을 추가한다.
uploadedFileCount를filesToUpload.length와 비교하여 불일치 시 실패로 처리하거나,checkTileUploading이전에 업로드 완료 개수를 tesla API로 검증한다. - transient S3/transport 실패(500/ECONNRESET/499)에 대해 파일 단위 재업로드(idempotent PUT이므로 안전) 백오프 재시도를 SDK 재시도 위에 얇게 추가하는 방안을 검토한다.
Monitoring#
업로드 실패 발생 추이:
service:cupixworks-pano-postprocessor-instance status:error "AwsS3Manager::uploadDirectoryByCredential | upload failed"
pano resize 단계 전체 실패 추이 (상위 catch):
service:cupixworks-pano-postprocessor-instance status:error "PanoPostprocessorService::run | resize pano"
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard (부분 실패 전파 로직 추가 시)