AwsS3Manager#downloadParallel swallows S3 download errors
RCA: ResizeWork::resizePano - Input file contains unsupported image format
Overview#
What Happened#
2026-05-08 21:15~21:16 UTC에 cupixworks-pano-postprocessor-instance 서비스에서 pano 이미지 리사이즈 처리 중 sharp 라이브러리가 "Input file contains unsupported image format" 에러를 발생시켰다. capture 694300 (gilbaneco 팀) 배치 작업에서 pano 83954896, 83954807 두 건이 영향을 받았으며, 총 10회의 에러 로그가 기록되었다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Error |
| exception.message | Input file contains unsupported image format |
| top_frame | app.cjs:6592 (ResizeWork.resizePano) |
| runtime | Node.js (bundled as app.cjs) |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| gilbaneco | 10 | pano 2건의 리사이즈 실패, 타일 업로드 미완료로 해당 파노라마 뷰어 표시 불가 |
Timeline#
- 21:10:14Z — PanoPostprocessorService 시작, 인증 완료
- 21:10:25~27Z — S3 다운로드 중 "The specified key does not exist" 에러 2건 발생
- 21:10:37Z — download_panos 단계 완료 (15163ms, 에러 무시됨)
- 21:15:20Z — pano 83954896 리사이즈 시 sharp metadata 호출 실패
- 21:16:28Z — pano 83954807 리사이즈 시 동일 에러 발생
- 21:20:28Z — resize_panos 단계 완료 (379428ms)
Error Log#
PanoPostprocessorService::run | resize pano id:83954896 | error {"stack":"Error: ResizeWork::resizePano - error
at ResizeWork.resizePano (/tmp/agent/dist/app.cjs:6596:13)
at async /tmp/agent/dist/app.cjs:6880:15","message":"ResizeWork::resizePano - error"}
Impact#
- Service:
cupixworks-pano-postprocessor-instance - Team: gilbaneco
- 발생 횟수: 10
- 최초 발생: 2026-05-08T21:15:20.415Z
- 최근 발생: 2026-05-08T21:16:28.968Z
Root Cause Summary#
S3에서 pano 원본 이미지를 다운로드할 때 "The specified key does not exist" 에러가 발생했으나, AwsS3Manager::downloadParallel 메서드가 에러를 catch하고 로그만 남긴 후 정상 완료 처리했다. 이로 인해 다운로드가 실패한 pano의 로컬 파일이 존재하지 않거나 비어있는 상태에서 후속 resize 단계가 실행되었고, sharp 라이브러리가 metadata를 읽으려 할 때 "Input file contains unsupported image format" 에러를 발생시켰다. 실제 이미지 포맷 문제가 아닌, S3 키 누락으로 인한 다운로드 실패가 전파되지 않은 것이 근본 원인이다.
Technical Analysis#
Code Path#
- Entry point:
pano-postprocessor-service.ts:79—run()메서드 - Download:
pano-postprocessor-service.ts:117—awsS3Manager.downloadParallel(cpPanos)호출 - Download failure suppression:
aws-s3.manager.ts:75-77— catch 블록에서 로그만 남기고 에러 전파하지 않음 - Resize trigger:
pano-postprocessor-service.ts:168—resizeWork.resizePano(cpPano)호출 - Failure point:
resize-work.ts:24—sharp(cpPano.downloadImagePath).metadata()호출 시 에러
다운로드 단계에서 에러를 삼키는 코드:
const tasks = cpPano.map(cpPano => {
return limit(async () => {
try {
logger.debug('AwsS3Manager::downloadParallel | buket region, name, key, endpoint, download path %s %s %s %s %s', cpPano.panoBucketRegion, cpPano.panoBucketName, cpPano.panoBucketKey, cpPano.panoBucketEndpoint, cpPano.downloadImagePath);
if (cpPano.panoBucketEndpoint) {
await this.downloadByPanoApi(cpPano);
} else {
await this.download(
cpPano.panoBucketRegion!,
cpPano.panoBucketName!,
cpPano.panoBucketKey!,
cpPano.downloadImagePath!
);
}
} catch (err) {
logger.error('AwsS3Manager::downloadParallel | download failed - pano: %s, error: %s', cpPano.downloadImagePath, (err as Error).message);
}
});
});
기대 동작: 다운로드 실패 시 해당 pano를 에러 상태로 마킹하거나, 후속 처리에서 제외해야 한다. 실제 동작: catch 블록에서 에러를 로그만 남기고 삼킨다. 다운로드 실패 여부가 호출자에게 전달되지 않는다.
리사이즈 단계에서 실패하는 코드:
async resizePano(cpPano: CPPano): Promise<void> {
logger.debug('ResizeWork::resizePano | begin');
let metadata = null;
const sharpOptions = { limitInputPixels: false };
try {
if (!cpPano.panoId || !cpPano.downloadImagePath) return;
metadata = await sharp(cpPano.downloadImagePath, sharpOptions).metadata();
logger.debug('ResizeWork::resizePano | stitched pano metadata - pano id: %d, width: %d, height: %d', cpPano.panoId, metadata.width, metadata.height);
} catch (error: any) {
logger.error('ResizeWork::resizePano | error: %s', JSON.stringify(error, Object.getOwnPropertyNames(error)));
throw new Error('ResizeWork::resizePano - error');
}
기대 동작: 유효한 이미지 파일에서 metadata를 읽어 리사이즈 수행. 실제 동작: 다운로드 실패로 파일이 없거나 비어있어 sharp가 "unsupported image format" 에러를 던짐.
서비스 런 메서드의 resize 에러 핸들링:
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!);
}
});
});
resize 단계의 catch에서 pano를 Error 상태로 마킹하므로 데이터 정합성은 유지되지만, 에러 메시지가 실제 원인(S3 키 누락)이 아닌 증상(unsupported image format)을 보고한다.
Log Evidence#
S3 다운로드 에러 로그 (동일 job/capture에서 발생):
query: service:cupixworks-pano-postprocessor-instance status:error "The specified key does not exist"
time: 2026-05-08T21:00:00Z to 2026-05-08T21:20:00Z
2026-05-08T21:10:25.306Z [ERROR] AwsS3Manager::downloadParallel | begin - "The specified key does not exist."
2026-05-08T21:10:27.908Z [ERROR] AwsS3Manager::downloadParallel | begin - "The specified key does not exist."
다운로드 단계 완료 후 리사이즈 실패:
query: service:cupixworks-pano-postprocessor-instance "ResizeWork::resizePano"
time: 2026-05-08T21:14:00Z to 2026-05-08T21:17:00Z
2026-05-08T21:15:20.414Z [ERROR] ResizeWork::resizePano | error: {"stack":"Error: Input file contains unsupported image format\n at Sharp.metadata (/tmp/agent/dist/node_modules/sharp/lib/input.js:487:17)\n at ResizeWork.resizePano (/tmp/agent/dist/app.cjs:6592:90)","message":"Input file contains unsupported image format"}
2026-05-08T21:15:20.415Z [ERROR] PanoPostprocessorService::run | resize pano id:83954896 | error {"stack":"Error: ResizeWork::resizePano - error\n at ResizeWork.resizePano (/tmp/agent/dist/app.cjs:6596:13)\n at async /tmp/agent/dist/app.cjs:6880:15","message":"ResizeWork::resizePano - error"}
2026-05-08T21:16:28.968Z [ERROR] ResizeWork::resizePano | error: {"stack":"Error: Input file contains unsupported image format\n at Sharp.metadata ...","message":"Input file contains unsupported image format"}
2026-05-08T21:16:28.968Z [ERROR] PanoPostprocessorService::run | resize pano id:83954807 | error ...
Job 메타데이터:
- capture.id: 694300
- job.id: 1059879
- team: gilbaneco (id: 780)
- user: gdossii@gilbaneco.com (id: 43452)
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | S3 키 누락으로 다운로드 실패 → 빈/비정상 파일이 resize에서 sharp 에러 유발 | S3 "key does not exist" 에러 2건이 동일 job에서 resize 실패 5분 전 발생 (21:10:25Z, 21:10:27Z). downloadParallel catch 블록이 에러를 삼킴 (aws-s3.manager.ts:75-77). 실패한 pano 수(2)와 S3 에러 수(2) 일치 |
— | Confirmed |
| H2 | 실제로 지원되지 않는 이미지 포맷(HEIF, AVIF 등)이 업로드됨 | sharp의 에러 메시지가 "unsupported image format"임 | S3 키 자체가 존재하지 않으므로 파일을 다운로드할 수 없었음. 동일 capture의 다른 pano들은 정상 처리됨. CPPano 모델이 확장자를 .jpg로 하드코딩(cppano.ts:36) |
Rejected |
| H3 | 네트워크 문제로 다운로드가 중간에 끊겨 불완전한 파일 생성 | 불완전한 파일도 sharp에서 동일 에러 유발 가능 | "The specified key does not exist"는 네트워크가 아닌 S3 객체 부재를 의미. download() 메서드는 stream error 시 writeStream을 destroy하므로 불완전 파일 가능성 낮음 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
aws-s3.manager.ts:60-78:downloadParallel의 catch 블록에서 다운로드 실패한 pano를 추적하도록 변경. 실패 시 해당 cpPano에 다운로드 실패 플래그를 설정하거나, 실패 목록을 반환하여 후속 단계에서 해당 pano를 건너뛸 수 있게 해야 한다.resize-work.ts:23:resizePano진입 시downloadImagePath파일이 실제 존재하는지fs.existsSync체크를 추가하여, 파일이 없을 경우 명확한 에러 메시지("download file not found")를 던지도록 개선.
단기 개선 (1주 이내)#
pano-postprocessor-service.ts:117이후에 다운로드 실패한 pano를erroredPanoIds에 추가하고, resize/mask/infer 단계에서 해당 pano를 skip하는 로직 추가. 이를 통해 불필요한 처리 시도와 혼란스러운 에러 메시지를 방지.- 에러 메시지 개선:
ResizeWork::resizePano에서 catch 시 원본 에러의 message를 포함하여 re-throw하도록 변경 (throw new Error(\ResizeWork::resizePano - error: ${error.message}`)`) — 현재는 원인 정보가 소실됨.
장기 개선 (재발 방지)#
- S3 원본 파일 부재 원인 조사: pano가 postprocessor에 전달되었지만 S3에 원본이 없는 경우는 업로드 파이프라인 문제를 시사. 업로드 완료 확인 후에만 postprocessor job을 트리거하도록 보장하는 메커니즘 검토.
- 다운로드 단계에서 재시도(retry) 로직 추가를 고려. 일시적 S3 접근 오류의 경우 1~2회 재시도로 해결될 수 있음.
Monitoring#
service:cupixworks-pano-postprocessor-instance "The specified key does not exist"— S3 키 누락 빈도 추적service:cupixworks-pano-postprocessor-instance "unsupported image format"— 포맷 에러 빈도 (다운로드 실패 연동 여부 확인)- 메트릭 추가 권장:
pano.postprocess.download_failed카운터를downloadParallel에서 방출하여 다운로드 실패율을 별도 추적
@service:cupixworks-pano-postprocessor-instance "download failed" OR "key does not exist"
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard — 다운로드 실패 전파 로직 추가는 기존 에러 핸들링 패턴(
erroredPanoIds)을 확장하는 수준이므로 영향 범위가 제한적이나, 업스트림 S3 키 누락 원인은 별도 조사가 필요함.