ES /docs

[CupixAerialMap] preprocess fail Invalid URL

RCA: [CupixAerialMap] preprocess fail Invalid URL

Overview#

What Happened#

2026-07-11 03:06:42 KST, production aerial-map-service ECS/Fargate preprocess task (aerial map 530, team weoneil) 가 병렬 photo 업로드 루프에서 download_urlnull 인 aerial photo 를 만나 axios URL 파서에서 TypeError [ERR_INVALID_URL] 을 throw 했다. 이 클러스터의 로그([CupixAerialMap] preprocess fail Invalid URL)는 preprocess/index.ts:148 의 inner catch 가 원본 error 를 rethrow 하기 직전에 남긴 첫 번째 에러 라인이며, 7 초 뒤 outer IIFE catch 가 [CupixAerialMap] preprocess fail - error:({"input":"null","code":"ERR_INVALID_URL"}) / message:(Invalid URL) 로그(sibling cluster 3c48c1b1)를 남기고 exit code 1 로 종료했다.

Quick Facts#

Field Value
exception.class TypeError [ERR_INVALID_URL] (Node.js URL 파서)
exception.message Invalid URL ({"input":"null","code":"ERR_INVALID_URL"})
top_frame applications/aerial-map-service/src/code/src/preprocess/index.ts:148 (inner catch), 실제 throw 지점 preprocess/index.ts:89
runtime Node.js on ECS Fargate (cupix-aerial-map-preprocess:prod, 1024 CPU / 2048 MB)
deploy image sha256:0a00e602c0acad71bece8834eaf12e41923f381a9869c7d1a1842d86f2dc6647
env production, us-west-2 (task 0345f9217ffa4dd1bc65cbbc2c6b28d0)

Affected Teams#

Team / Domain Error Count Impact
weoneil (aerial map 530) 1 Aerial map preprocessing 실패, Pix4D project 생성됐으나 image 업로드 미완료 (orphan project)

Timeline#

  1. 2026-07-11 03:01:53 KST — Fargate task 기동 (method: nadir, outputs: [orthomosaic, pointcloud, mesh, dsm], resolution: high).
  2. 2026-07-11 03:04:01 KSTpreprocess/index.ts:52 downloadStreamByUrl(firstAerialPhoto.downloadUrl) 성공, EXIF 파싱 (Hasselblad L3D-100c).
  3. 2026-07-11 03:04:04 KST — Pix4D project cupix-production-weoneil-530-1783706644905 생성.
  4. 2026-07-11 03:06:42 KST — 병렬 업로드 루프의 axios 가 null URL 에서 ERR_INVALID_URL throw → preprocess/index.ts:148 inner catch 가 [CupixAerialMap] preprocess fail Invalid URL 로그 (이 클러스터).
  5. 2026-07-11 03:06:49 KST — Rethrow 된 error 를 outer IIFE catch(preprocess/index.ts:174-178) 가 잡아 preprocess fail - error:(...) / message:(Invalid URL) 로그 (sibling cluster 3c48c1b1).
  6. 2026-07-11 03:07:48 KST — Step Functions preprocess-checkStates.TaskFailed / ExitCode: 1 감지.
  7. 2026-07-11 03:07:56 KSTcupix-process-fail-production-vdcopix4d process fail: fail 기록.
  8. 2026-07-11 03:08:06 KST — Data collect finalize.

Error Log#

Datadog Logs

text
[CupixAerialMap] preprocess fail Invalid URL

Impact#

  • Service: aerial-map-service
  • 발생 횟수: 1 (동일 ECS task 에서 sibling cluster 3c48c1b1-6622-4e52-bb9b-7c95b7410476 1건 추가 — 같은 실패의 outer catch 로그)
  • 최초 발생: 2026-07-11 03:06:42 KST
  • 최근 발생: 2026-07-11 03:06:42 KST
  • 비즈니스 영향: weoneil 팀 aerial map 530 preprocessing 실패, Pix4D project 는 생성됐으나 image 업로드 미완료로 orphan 상태.

Root Cause Summary#

cupixApi.getAerialPhotosByAerialMapId(530) 가 반환한 aerial photo 목록에 download_urlnull 인 항목이 최소 한 개 포함되어 있었다. preprocess/index.ts:82-101pLimit(10) 병렬 업로드 루프는 검증 없이 aerialPhoto.downloadUrldownloadBufferByUrl(downloadUrl)axios({ url: null }) 로 전달했고, Node.js/axios URL 파서가 TypeError [ERR_INVALID_URL] 을 throw 했다. 이 클러스터의 로그 문자열(preprocess fail Invalid URL)은 inner try/catch(preprocess/index.ts:144-153) 가 error instanceof AerialMapError 검사에 실패하면서 else 브랜치(line 148) 로 진입해 `[CupixAerialMap] preprocess fail $\{error.message\}` 템플릿을 그대로 로그로 남긴 결과다. 첫 사진(sortedAerialPhotos[0]) 은 유효했기 때문에 EXIF 파싱과 Pix4D 프로젝트 생성까지는 성공했고, 실패는 이후 병렬 업로드에 진입한 두 번째 이상의 photo 중 하나의 null URL 때문이다. 이 클러스터와 sibling 3c48c1b1 은 동일 실행의 두 catch 지점에서 나온 로그이므로 root cause 는 동일하다.

Technical Analysis#

Code Path#

Entry point: applications/aerial-map-service/src/code/src/preprocess/index.ts:168 (IIFE 시작)

  1. app(input) 호출 → preprocess/index.ts:48 cupixApi.getAerialPhotosByAerialMapId(530).
  2. preprocess/index.ts:50-52 — 첫 사진(sortedAerialPhotos[0]) 의 URL 로 EXIF 파싱 성공.
  3. preprocess/index.ts:82-101pLimit(10) 병렬 업로드 태스크 생성.
  4. Failure point: preprocess/index.ts:89 downloadBufferByUrl(downloadUrl)downloadUrl === null 인 항목에서 axios 가 ERR_INVALID_URL throw.
  5. preprocess/index.ts:144-153 inner catch — error instanceof AerialMapError === false 이므로 else 브랜치(147-150) 진입, line 148 이 이 클러스터의 로그를 emit, AMB710saveError 로 저장한 뒤 rethrow.
  6. preprocess/index.ts:174-178 outer IIFE catch — rethrow 된 error 를 JSON.stringify(error) 로 포맷팅해 sibling cluster 로그(3c48c1b1) 를 emit, process.exit(1).

이 클러스터를 emit 하는 지점:

applications/aerial-map-service/src/code/src/preprocess/index.ts:144-153typescript
  } catch (error: any) {
    if (error instanceof AerialMapError) {
      await cupixApi.saveError(aerialMapId, error.code, error.reason);
    } else {
      logger.error(`[CupixAerialMap] preprocess fail ${error.message}`);
      await cupixApi.saveError(aerialMapId, ERROR_CODE['AMB710'].code, ERROR_CODE['AMB710'].reason);
    }

    throw error;
  }

error.message === "Invalid URL" 이므로 실제 로그 문자열은 "[CupixAerialMap] preprocess fail Invalid URL" 이 되며, 이 클러스터의 representative error 와 정확히 일치한다.

병렬 업로드 루프 — 실제 throw 지점:

applications/aerial-map-service/src/code/src/preprocess/index.ts:82-101typescript
    const limit = pLimit(10);
    const uploadTasks = sortedAerialPhotos.map((aerialPhoto) =>
      limit(async () => {
        logger.debug(`[CupixAerialMap] download and upload aerial photo: ${aerialPhoto.id}`);
        const downloadUrl = aerialPhoto.downloadUrl;
        const imageKey = `${credential.key}/${PIX4D_PHOTO_FILENAME_PREFIX}_${aerialPhoto.id}`;
        imageKeys.push(imageKey);
        const buffer = await downloadBufferByUrl(downloadUrl);
        const exifStream = Readable.from(buffer);
        const photoExif = await readPhotoExif(exifStream);
        const uploadStream = Readable.from(buffer);
        await uploadS3StreamByKey({ credential, imageKey, stream: uploadStream });
        // ...
      }),
    );

downloadUrlstring 이라는 타입 가정 하에 검증 없이 axios 로 전달:

applications/aerial-map-service/src/code/src/common/s3.ts:159-167typescript
export const downloadBufferByUrl = async (downloadUrl: string) => {
  const response = await axios({ method: 'get', url: downloadUrl, responseType: 'arraybuffer' });
  return Buffer.from(response.data);
};

export const downloadStreamByUrl = async (downloadUrl: string) => {
  const response = await axios({ method: 'get', url: downloadUrl, responseType: 'stream' });
  return response.data;
};

Outer IIFE catch (sibling cluster 3c48c1b1 을 emit) — 이 클러스터의 rethrow 를 잡는 지점:

applications/aerial-map-service/src/code/src/preprocess/index.ts:168-179typescript
(async () => {
  try {
    await app(input);
    logger.info(`[CupixAerialMap] preprocess success`);
    await sleep(30000);
    process.exit(0);
  } catch (error: any) {
    logger.error(`[CupixAerialMap] preprocess fail - error:(${JSON.stringify(error)}) / message:(${error.message})`);
    await sleep(30000);
    process.exit(1);
  }
})();

기대 동작: 모든 aerial photo 는 유효한 download_url (S3 presigned URL) 을 가진다. 실제 동작: 최소 하나의 사진이 download_url: null 을 반환해 axios URL 파서가 throw. 어떤 photo ID 가 null URL 이었는지는 logger.debug (index.ts:85) 라인이 debug 레벨이라 Datadog 에 저장되지 않아 이 로그만으로는 특정 불가 — uncertain, needs verification (Cupix Watch / Kibana 또는 DB 조회 필요).

Log Evidence#

Datadog 쿼리 (재현용):

text
service:aerial-map-service "preprocess fail Invalid URL"
from: 2026-07-10T17:30:00Z
to:   2026-07-10T19:00:00Z

이 쿼리로 확인된 이 클러스터의 유일한 로그:

text
2026-07-10T18:06:42Z ERROR [CupixAerialMap] preprocess fail Invalid URL

같은 ECS task 의 인접 로그 시퀀스 (service:aerial-map-service "CupixAerialMap" between 2026-07-10T18:05:00Z and 2026-07-10T18:10:00Z):

text
2026-07-10T18:06:42Z ERROR [CupixAerialMap] preprocess fail Invalid URL              ← 이 클러스터 (inner catch, index.ts:148)
2026-07-10T18:06:49Z ERROR [CupixAerialMap] preprocess fail - error:({"input":"null","code":"ERR_INVALID_URL"}) / message:(Invalid URL)   ← sibling cluster 3c48c1b1 (outer catch, index.ts:175)
2026-07-10T18:07:48Z INFO  [CupixAerialMap] preprocess-check - event({"Error":"States.TaskFailed", ... "ExitCode":1 ...})
2026-07-10T18:07:56Z INFO  [CupixAerialMap] cupix-process-fail-production-vdco - pix4d process fail: fail
2026-07-10T18:08:06Z INFO  [CupixAerialMap] cupix-data-collect-production-vdco - aerial map(530) data collected

Step Functions preprocess-check 이벤트 payload 에 포함된 ECS task 정보:

json
{
  "TaskArn": "arn:aws:ecs:us-west-2:002596530511:task/cupix-aerial-map-ecs-production/0345f9217ffa4dd1bc65cbbc2c6b28d0",
  "ExitCode": 1,
  "StopCode": "EssentialContainerExited",
  "StoppedReason": "Essential container in task exited",
  "Image": "002596530511.dkr.ecr.us-west-2.amazonaws.com/cupix-aerial-map-preprocess:prod",
  "ImageDigest": "sha256:0a00e602c0acad71bece8834eaf12e41923f381a9869c7d1a1842d86f2dc6647",
  "Overrides": {
    "aerialMap": { "id": 530, "processing_option": { "method": "nadir", "resolution": "high" } },
    "team": { "domain": "weoneil" }
  }
}

이 클러스터의 로그(03:06:42)와 sibling cluster 의 로그(03:06:49)의 7 초 간격은 cupixApi.saveError(aerialMapId, 'AMB710', ...) HTTP 호출 + rethrow → outer catch 진입에 걸린 시간으로 설명된다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 이 클러스터는 preprocess/index.ts:148 inner catch 가 ERR_INVALID_URL (message: "Invalid URL") 을 잡아 emit 한 로그이며, root cause 는 aerial photo 의 downloadUrl === null 로그 문자열이 `[CupixAerialMap] preprocess fail $\{error.message\}` 템플릿과 정확히 일치. Sibling cluster 3c48c1b1 의 payload \{"input":"null","code":"ERR_INVALID_URL"\} 가 URL 파서 실패임을 명시. 두 로그가 같은 ECS task(0345f9217...) 에서 7 초 간격으로 발생 Confirmed
H2 이 클러스터가 별개 원인 (예: 다른 axios 호출, 다른 URL 파싱 지점) 에서 발생 Invalid URLpreprocess/index.ts 여러 axios 호출 중 어느 것에서도 나올 수 있음 같은 task 의 sibling log 가 7 초 뒤 outer catch 에서 ERR_INVALID_URL payload 로 나옴. Inner catch 는 rethrow 하므로 두 로그는 동일 error 인스턴스에서 파생. Grep 결과 preprocess fail Invalid URLpreprocess/index.ts:148 한 곳에서만 emit 가능 Rejected
H3 preprocess/index.ts:52 downloadStreamByUrl(firstAerialPhoto.downloadUrl) 가 실패한 경우 (첫 사진의 URL null) input: "null" 문자열이 정확히 매칭 03:04:01 KST 에 camera maker: Hasselblad 로그 확인 → line 52 통과. 실패는 그 뒤 2 분 30 초 후 병렬 루프에서 발생 Rejected
H4 S3 presigned URL 만료로 인한 axios 실패 병렬 업로드 지연 시 만료 이론적 가능 만료된 URL 은 4xx HTTP 응답이지 파서 단계의 ERR_INVALID_URL 아님. input: "null" payload 는 URL 파서 argument 가 null 이었음을 명시 Rejected
H5 Aerial photo 중 하나가 state != 'uploaded' 이라 backend 가 download_url: null 을 반환 api.ts:1024-1035 mapping 에서 download_url pass-through, backend 스펙상 미완료 photo 는 URL null 가능 실제 photo state / DB 조회 필요 — 로그만으로 확정 불가 Inconclusive (원인 가능성 있음, backend 조회 필요)

Fix Recommendation#

즉시 조치 (Critical)#

  • applications/aerial-map-service/src/code/src/preprocess/index.ts:86-89 병렬 업로드 진입 전 각 aerialPhoto.downloadUrl 유효성 검증. 방향:
    • Null/empty 이면 신규 error code (예: AMB711 Missing download URL for aerial photo) 로 AerialMapError throw → inner catch 의 error instanceof AerialMapError 브랜치를 타서 사용자향 오류 메시지 확보.
    • 실패한 aerialPhoto.id 를 error 메시지에 포함 (`Missing download URL for aerial photo $\{aerialPhoto.id\}`) 하여 debug 로그 없이도 특정 가능.
  • applications/aerial-map-service/src/code/src/preprocess/index.ts:52 downloadStreamByUrl(firstAerialPhoto.downloadUrl) 도 동일 검증 추가 (지금은 index 0 이 우연히 유효했을 뿐).
  • applications/aerial-map-service/src/code/src/common/api.ts:1024-1035 getAerialPhotosByAerialMapId mapping 에서 download_url null 항목을 로그 남기고 필터링하거나 명시적 에러로 승격 — 백엔드 스펙 확인 후 결정.

단기 개선 (1주 이내)#

  • applications/aerial-map-service/src/code/src/preprocess/index.ts:148 inner catch 의 로그를 `[CupixAerialMap] preprocess fail ($\{error.name\} $\{error.code ?? ''\}): $\{error.message\}` 형태로 확장 — 현재는 error.message 만 출력해 ERR_INVALID_URL 같은 code 를 이 라인만으로는 확인 불가 (outer catch 로그를 봐야 함).
  • preprocess/index.ts:175 outer catch 의 JSON.stringify(error) 는 Error instance 를 제대로 직렬화하지 못한다 (stack, name non-enumerable). error.stack, error.name, error.code, error.message 를 명시적으로 로깅.
  • IAerialPhoto.downloadUrl 타입(type/type.ts:174-181) 을 string | null 로 좁혀 컴파일 타임 null-check 강제.
  • logger.debug("download and upload aerial photo: ${aerialPhoto.id}") (index.ts:85) 를 info 레벨로 승격하거나, 배치 시작 시 [CupixAerialMap] uploading N photos: [ids...] info 로그 추가 → Datadog 만으로 실패 photo 특정 가능하게.

장기 개선 (재발 방지)#

  • Backend (tesla) aerial_photos API 응답 스펙 확정: state != 'uploaded' 항목 서버 측 필터링, 또는 download_url null 포함 시 422 반환.
  • Preprocess 시작 전 aerial_map 의 photo state 를 검사하는 preflight Step Functions choice state 도입.
  • 실패 시 orphan Pix4D project 를 정리하는 compensating step (preprocess/index.ts:75 에서 생성한 projectId 를 실패 경로에서 삭제).

Monitoring#

  • 이 클러스터 지속 발생 확인용 timeseries:
text
service:aerial-map-service status:error "preprocess fail Invalid URL"
  • 전체 ERR_INVALID_URL 추적 (sibling cluster 포함):
text
service:aerial-map-service status:error "ERR_INVALID_URL"
  • Preprocess 실패 전반:
text
service:aerial-map-service status:error "[CupixAerialMap] preprocess fail"
  • Downstream Step Functions 실패 참조:
text
service:aerial-map-service "pix4d process fail" @environment:production
  • 알림 임계: [CupixAerialMap] preprocess fail 1 시간 내 3건 이상이면 Slack #alerts.

Risk Assessment#

  • Risk level: medium — 단일 팀(weoneil) 단일 aerial map (530) 실패로 시스템 전체 위협은 아니나, 사용자 재시도 시에도 동일 photo 의 null URL 이면 계속 실패. 사용자 개입 (photo 재업로드) 필요할 수 있음.
  • 예상 복잡도: standard — 3~4 개 지점에 null guard 추가 및 error code 신설. Backend 필터링 병행 시 조정 필요. Sibling cluster 3c48c1b1 의 fix 와 동일한 코드 변경으로 함께 해결됨.