ES /docs

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#

  1. 21:10:14Z — PanoPostprocessorService 시작, 인증 완료
  2. 21:10:25~27Z — S3 다운로드 중 "The specified key does not exist" 에러 2건 발생
  3. 21:10:37Z — download_panos 단계 완료 (15163ms, 에러 무시됨)
  4. 21:15:20Z — pano 83954896 리사이즈 시 sharp metadata 호출 실패
  5. 21:16:28Z — pano 83954807 리사이즈 시 동일 에러 발생
  6. 21:20:28Z — resize_panos 단계 완료 (379428ms)

Error Log#

Datadog Logs

text
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:79run() 메서드
  • Download: pano-postprocessor-service.ts:117awsS3Manager.downloadParallel(cpPanos) 호출
  • Download failure suppression: aws-s3.manager.ts:75-77 — catch 블록에서 로그만 남기고 에러 전파하지 않음
  • Resize trigger: pano-postprocessor-service.ts:168resizeWork.resizePano(cpPano) 호출
  • Failure point: resize-work.ts:24sharp(cpPano.downloadImagePath).metadata() 호출 시 에러

다운로드 단계에서 에러를 삼키는 코드:

applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:60-78typescript
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 블록에서 에러를 로그만 남기고 삼킨다. 다운로드 실패 여부가 호출자에게 전달되지 않는다.

리사이즈 단계에서 실패하는 코드:

applications/agents/packages/cupix-pano-postprocessor/src/work/resize-work.ts:17-29typescript
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 에러 핸들링:

applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:165-178typescript
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에서 발생):

text
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
text
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."

다운로드 단계 완료 후 리사이즈 실패:

text
query: service:cupixworks-pano-postprocessor-instance "ResizeWork::resizePano"
time: 2026-05-08T21:14:00Z to 2026-05-08T21:17:00Z
text
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 메타데이터:

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에서 방출하여 다운로드 실패율을 별도 추적
text
@service:cupixworks-pano-postprocessor-instance "download failed" OR "key does not exist"

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — 다운로드 실패 전파 로직 추가는 기존 에러 핸들링 패턴(erroredPanoIds)을 확장하는 수준이므로 영향 범위가 제한적이나, 업스트림 S3 키 누락 원인은 별도 조사가 필요함.