ES /docs

[CupixAerialMap] preprocess fail - error:({"input":"null","code":"ERR_INVALID_URL"}) / message:(Inva

RCA: [CupixAerialMap] preprocess fail - ERR_INVALID_URL

Overview#

What Happened#

2026-07-11 03:06 KST, production aerial-map-service ECS/Fargate task 가 aerial map 530 (team weoneil) 를 preprocess 하다가 axios ERR_INVALID_URL (input: "null") 로 실패하고 exit code 1 로 종료했다. Step Functions state machine 은 preprocess-check 단계에서 States.TaskFailed 로 감지해 cupix-process-fail-production-vdco 경로로 라우팅했고, aerial map 은 처리 실패 상태로 저장되었다.

Quick Facts#

Field Value
exception.class TypeError [ERR_INVALID_URL] (Node.js URL parser)
exception.message Invalid URL ({"input":"null","code":"ERR_INVALID_URL"})
top_frame applications/aerial-map-service/src/code/src/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 (subnet subnet-065f0fc20edc819ed, task 0345f9217ffa4dd1bc65cbbc2c6b28d0)

Affected Teams#

Team / Domain Error Count Impact
weoneil (aerial map 530) 1 Aerial map preprocessing 실패, Pix4D 처리 시작되지 않음

Timeline#

  1. 2026-07-11 03:00:42 KST — Step Functions navigator 가 aerial map 530 을 preprocess 경로로 라우팅 (cupix-navigate-production-vdco).
  2. 2026-07-11 03:01:53 KST — Fargate 태스크 기동, method: nadir, outputs: [orthomosaic, pointcloud, mesh, dsm], resolution: high.
  3. 2026-07-11 03:04:01 KST — 첫 번째 aerial photo EXIF 파싱 성공 (camera maker: Hasselblad, model: L3D-100c) — preprocess/index.ts:52downloadStreamByUrl(firstAerialPhoto.downloadUrl) 은 정상 통과.
  4. 2026-07-11 03:04:04 KST — Pix4D project 생성됨 (cupix-production-weoneil-530-1783706644905).
  5. 2026-07-11 03:06:42 KST — 병렬 photo 업로드 루프에서 첫 번째 로그 [CupixAerialMap] preprocess fail Invalid URL (cluster b2583dda).
  6. 2026-07-11 03:06:49 KST — 상위 IIFE catch 블록의 [CupixAerialMap] preprocess fail - error:({"input":"null","code":"ERR_INVALID_URL"}) / message:(Invalid URL) (cluster 3c48c1b1).
  7. 2026-07-11 03:07:48 KST — Step Functions preprocess-checkStates.TaskFailed / ExitCode: 1 감지.
  8. 2026-07-11 03:07:56 KSTcupix-process-fail-production-vdcopix4d process fail: fail 을 기록.
  9. 2026-07-11 03:08:06 KST — Data collect finalize.

Error Log#

[Datadog Logs](https://app.datadoghq.com/logs?query=service%3Aaerial-map-service%20status%3Aerror%20%40environment%3Aproduction%20%22%5BCupixAerialMap%5D%20preprocess%20fail%20-%20error%3A(%7Binput%3Anull%2Ccode%3AERR_INVALID_URL%7D)%20%2F%20message%3A(Inva%22&from_ts=1783703160000&to_ts=1783710420000&live=false)

text
[CupixAerialMap] preprocess fail - error:({"input":"null","code":"ERR_INVALID_URL"}) / message:(Invalid URL)

Impact#

  • Service: aerial-map-service
  • 발생 횟수: 1 (동일 태스크에서 sibling cluster b2583dda-d82b-44ad-8e1b-a458fb5b9596 1건 추가 — 같은 실행의 두 개 다른 error 로그 라인)
  • 최초 발생: 2026-07-11 03:06:49 KST
  • 최근 발생: 2026-07-11 03:06:49 KST
  • 비즈니스 영향: weoneil 팀 aerial map 530 processing 실패, Pix4D project 는 생성되었으나 image 업로드가 완료되지 않아 orphan project 발생.

Root Cause Summary#

CupixApi.getAerialPhotosByAerialMapId 가 반환한 aerial photo 목록에 download_urlnull 인 항목이 포함되어 있었다. preprocess/index.ts:89 의 병렬 업로드 루프가 이 값을 그대로 downloadBufferByUrl(downloadUrl)axios({ url: null }) 로 넘겼고, Node.js/axios URL 파서가 TypeError [ERR_INVALID_URL] 을 던지면서 Fargate 태스크가 exit code 1 로 종료됐다. 첫 번째 사진(sortedAerialPhotos[0]) 의 downloadUrl 은 유효했기 때문에 EXIF 파싱과 Pix4D 프로젝트 생성까지는 성공했고, 이후 병렬 업로드 단계에서 두 번째 이후 사진 중 하나의 null URL 이 실패 원인이 되었다. 코드 경로 어디에도 downloadUrl 이 문자열인지 확인하는 검증이 없다.

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) 로 aerial photo 목록 조회.
  2. preprocess/index.ts:49-52 — 첫 사진에 대해 downloadStreamByUrl 로 EXIF 읽기 성공 (Hasselblad L3D-100c 로그 확인).
  3. preprocess/index.ts:82-101pLimit(10) 로 병렬 업로드 태스크 생성, 각 photo 의 downloadUrldownloadBufferByUrl 에 그대로 전달.
  4. Failure point: preprocess/index.ts:89downloadBufferByUrl(downloadUrl)downloadUrl === null 인 항목을 만나 axios 가 ERR_INVALID_URL 을 throw.
  5. preprocess/index.ts:144-152try/catch 는 error 를 잡아 AerialMapError 가 아니므로 AMB710 으로 saveError 후 rethrow.
  6. preprocess/index.ts:174-178 IIFE catch 가 rethrow 된 error 를 JSON.stringify(error) + error.message 로 포맷팅해 로그 출력, process.exit(1).

Photo 목록을 채우는 API mapping:

applications/aerial-map-service/src/code/src/common/api.ts:1024-1035typescript
return photos.map((photo: IAerialPhotoData) => {
  return {
    id: photo.attributes.id,
    name: photo.attributes.name,
    state: photo.attributes.state,
    downloadUrl: photo.attributes.download_url,
    capturedAt: photo.attributes.captured_at,
    longitude: photo.attributes.longitude,
    latitude: photo.attributes.latitude,
    altitude: photo.attributes.altitude,
  };
});

download_url 을 그대로 pass-through 한다 — null / undefined 검증 없음. Type IAerialPhoto.downloadUrl: string (아래) 은 런타임 값을 강제하지 못한다.

applications/aerial-map-service/src/code/src/type/type.ts:174-181typescript
export interface IAerialPhoto {
  id: number;
  name: string;
  state: string;
  downloadUrl: string;
  capturedAt: string;
  longitude: string;
  latitude: string;
  altitude: string;
}

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

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 });
    // ...
  }),
);

axios 호출에서 URL 검증 없음:

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;
};

상위 catch — JSON.stringify(error) 는 TypeError 인스턴스에서 enumerable property (input, code) 만 직렬화하므로 로그에 {"input":"null","code":"ERR_INVALID_URL"} 이 남음:

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. 첫 사진은 유효했으므로 (readPhotoExif 성공) 문제는 batch 내 후속 사진 중 하나 — 어떤 사진 ID 인지는 로그에서 확인 불가 (logger.debug 라인이 debug 레벨이라 Datadog 에 저장되지 않음).

Log Evidence#

Datadog 쿼리 (재현용):

text
service:aerial-map-service "CupixAerialMap"
from: 2026-07-10T17:30:00Z
to:   2026-07-10T19:00:00Z

핵심 로그 시퀀스:

text
2026-07-10T18:01:53Z INFO  [CupixAerialMap] preprocess fargate memory limit: 2048 MB
2026-07-10T18:01:53Z INFO  [CupixAerialMap] aerial map method: nadir
2026-07-10T18:01:53Z INFO  [CupixAerialMap] aerial map outputs: ["orthomosaic","pointcloud","mesh","dsm"]
2026-07-10T18:01:53Z INFO  [CupixAerialMap] aerial map resolution: high
2026-07-10T18:04:01Z INFO  [CupixAerialMap] camera maker: Hasselblad, camera model: L3D-100c, captured at: 2026-07-09T16:32:41.000Z
2026-07-10T18:04:04Z INFO  [CupixAerialMap] create pix4d project - project name: cupix-production-weoneil-530-1783706644905
2026-07-10T18:06:42Z ERROR [CupixAerialMap] preprocess fail Invalid URL
2026-07-10T18:06:49Z ERROR [CupixAerialMap] preprocess fail - error:({"input":"null","code":"ERR_INVALID_URL"}) / message:(Invalid URL)
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

Step Functions 이벤트에 포함된 ECS 태스크 정보:

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",
  "Overrides": {
    "aerialMap": { "id": 530, "processing_option": { "method": "nadir", "resolution": "high" } },
    "team": { "domain": "weoneil" }
  }
}

주목할 gap: 18:04:04 (Pix4D project 생성) 와 18:06:42 (Invalid URL) 사이 약 2 분 30 초 동안 병렬 업로드가 진행되었으며, 이 구간의 logger.debug("download and upload aerial photo: ${aerialPhoto.id}") (index.ts:85) 는 debug 레벨이라 Datadog 에 없다. 어떤 photo ID 가 null URL 이었는지는 debug 로그 (Cupix Watch / Kibana) 나 DB 조회 없이는 특정 불가 — uncertain, needs verification.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Aerial photo 중 하나의 download_url 이 null 이어서 axios 가 ERR_INVALID_URL throw 에러 payload {"input":"null","code":"ERR_INVALID_URL"}, preprocess/index.ts:89 가 검증 없이 URL 전달, API mapping api.ts:1029 도 pass-through, 첫 사진(index 0) 은 정상 통과했음이 EXIF 로그로 증명 Confirmed
H2 첫 사진(sortedAerialPhotos[0]) 의 URL 이 null 이라 preprocess/index.ts:52 downloadStreamByUrl 에서 실패 input:"null" 문자열이 정확히 매칭 03:04:01 KST 에 camera maker: Hasselblad 로그가 있어 52 line 은 성공, EXIF 파싱까지 완료. 실패는 그 뒤 2 분 후 발생 Rejected
H3 S3 presigned URL 이 만료되어 axios 가 실패 병렬 업로드가 느려서 만료 가능성 이론적으로 존재 만료된 URL 이면 401/403 HTTP 에러이지 ERR_INVALID_URL 아님. 이 에러는 파싱 실패이지 네트워크 실패가 아님 Rejected
H4 Pix4D credential.key 가 null 이라 imageKey 문자열 조합에서 발생 ${credential.key}/${...} 도 코드에 존재 에러 code 는 ERR_INVALID_URL 로 URL 파서에서 발생. imageKey 는 S3 업로드 (uploadS3StreamByKey) 에 쓰이며 그전에 downloadBufferByUrl 에서 이미 실패 Rejected
H5 Aerial photo 가 state != 'uploaded' (예: pending, failed) 여서 backend 가 download_url null 을 반환 api.ts:1028 에서 state 필드도 가져오지만 검증 없이 반환. Cupix API 규약상 미완료 photo 는 URL 이 null 가능 실제 photo state 를 확인하려면 backend DB 조회 필요 — uncertain, needs verification Inconclusive (원인 가능성 있음, 검증 필요)

Fix Recommendation#

즉시 조치 (Critical)#

  • applications/aerial-map-service/src/code/src/preprocess/index.ts:89 병렬 업로드 진입 전 각 aerialPhoto.downloadUrl 유효성 검증. 방향:
    • Null/empty string 이면 명시적 AerialMapError (예: 신규 code AMB711 Missing download URL for aerial photo) throw 하여 원인 미상의 ERR_INVALID_URL 을 사용자향 에러 코드로 승격.
    • 실패한 photo ID 를 error 메시지에 포함시켜 debug 로그 없이도 어느 사진인지 특정 가능하게.
  • applications/aerial-map-service/src/code/src/common/api.ts:1024-1035 getAerialPhotosByAerialMapId 반환 mapping 에서도 download_url 이 null 인 항목을 필터링 또는 로그 남기고 반환. 방향은 백엔드 스펙 확인 후 결정 (필터 vs error).
  • preprocess/index.ts:52firstAerialPhoto.downloadUrl 도 동일한 검증 필요 (현재는 우연히 인덱스 0 이 유효해서 통과했을 뿐).

단기 개선 (1주 이내)#

  • applications/aerial-map-service/src/code/src/preprocess/index.ts:175 상위 catch 의 JSON.stringify(error) 는 Error instance 를 제대로 직렬화하지 못한다 (stack, name 이 non-enumerable). error.stack, error.name, error.code, error.message 를 명시적으로 로깅하도록 개선 → RCA 시 debug 로그 없이도 photo ID / URL 원본을 확인 가능하게.
  • IAerialPhoto.downloadUrl 타입을 string | null 로 좁혀 컴파일 타임에 사용처마다 null check 강제.
  • 실패한 aerial photo ID 를 cupixApi.saveError payload 에 포함시켜 사용자에게 "photo N number 재업로드 필요" 를 안내.

장기 개선 (재발 방지)#

  • Backend (tesla) aerial_photos API 응답에서 state != 'uploaded' 인 항목이 preprocess input 에 포함되지 않도록 서버 측 필터링. 또는 download_url 이 null 인 photo 를 포함할 때 backend 에서 422 를 반환.
  • Preprocess 시작 전 aerial_map 의 photo state 를 검사하는 preflight 단계 도입 (Step Functions choice state).
  • 병렬 업로드 loop 진입 시 [CupixAerialMap] uploading ${n} photos, first_id=..., last_id=... info 로그 추가 — 현재 debug 뿐이라 Datadog 만으로는 배치 크기와 순서 파악 불가.

Monitoring#

  • ECS Fargate 태스크 실패율 알림 (cupix-aerial-map-preprocess-task-def-production).
  • service:aerial-map-service status:error 타임시리즈 위젯:
text
service:aerial-map-service status:error @environment:production "[CupixAerialMap] preprocess fail"
  • ERR_INVALID_URL 만 별도로 추적:
text
service:aerial-map-service status:error "ERR_INVALID_URL"
  • 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) 에서 발생했고 배치 처리 실패로 사용자 경험 저하지만 시스템 전체 안정성 위협은 아님. Preprocess 재시도 시 동일 photo 가 여전히 null URL 이면 계속 실패 가능 — 사용자 개입 필요할 수 있음.
  • 예상 복잡도: standard — 3~4 개 지점에 null guard 추가 및 error code 신설. Backend 측 필터링을 함께 하면 조정 필요.