ES /docs

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

RCA: [CupixAerialMap] postprocess fail - ERR_INVALID_URL

Overview#

What Happened#

2026-04-23T07:35:06Z에 aerial-map-service의 postprocess 단계에서 aerial photo thumbnail 생성 중 ERR_INVALID_URL 에러가 발생했다. Aerial map ID 335 (tenant: qatest3)에 대해 Pix4D 처리는 성공했으나, aerial photo의 download_urlnull로 반환되어 URL 파싱에 실패했다. ECS Fargate 태스크가 exit code 1로 종료되었다.

Quick Facts#

Field Value
exception.class TypeError
exception.message Invalid URL
top_frame postprocess/process.ts:66
runtime Node.js (ECS Fargate, 2048 CPU / 8192 MB)
env production, us-west-2

Timeline#

  1. 2026-04-23T07:32:58Z — Postprocess Fargate 태스크 시작 (memory limit: 8192 MB)
  2. 2026-04-23T07:33:00Z — Pix4D output 조회 성공 (orthomosaic, mesh, dsm, flight_path 등)
  3. 2026-04-23T07:35:06ZprocessAerialPhoto에서 downloadStreamByUrl(null) 호출 시 ERR_INVALID_URL 발생
  4. 2026-04-23T07:35:06Z — Postprocess fail 로깅 후 exit code 1로 종료
  5. 2026-04-23T07:36:03Z — Step Functions postprocess-check가 States.TaskFailed 감지

Error Log#

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

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

Impact#

  • Service: aerial-map-service
  • 발생 횟수: 1
  • 최초 발생: 2026-04-23T07:35:06.338Z
  • 최근 발생: 2026-04-23T07:35:06.338Z

Aerial map ID 335의 postprocess가 실패하여 orthomosaic tile, DSM tile, mesh, pointcloud 등의 후처리 결과물이 사용자에게 전달되지 않았다. Pix4D 처리 자체는 성공했으므로 데이터 유실은 없으나, 사용자는 처리 결과를 확인할 수 없는 상태이다.

Root Cause Summary#

Cupix API의 getAerialPhotosByAerialMapId 응답에서 일부 aerial photo의 download_url 필드가 null로 반환되었다. processAerialPhoto 함수는 각 aerial photo에 대해 downloadStreamByUrl(aerialPhoto.downloadUrl)을 호출하는데, downloadUrlnull인 경우에 대한 검증이 없다. null 값이 문자열 "null"로 변환되어 axios 내부에서 new URL("null")이 호출되고, Node.js의 TypeError [ERR_INVALID_URL]이 발생했다. 이 에러는 processAerialPhoto 내부에서 catch되지 않고 상위 app() 함수의 catch로 전파되어 전체 postprocess가 실패했다.

Technical Analysis#

Code Path#

  • Entry point: postprocess/index.ts:172 — 메인 IIFE에서 app(input) 호출
  • postprocess/index.ts:71getAerialPhotosByAerialMapId(aerialMapId) 호출하여 aerial photo 목록 조회
  • postprocess/process.ts:56-66processAerialPhoto에서 각 aerial photo를 순회하며 thumbnail 생성
postprocess/process.ts:49-68typescript
export const processAerialPhoto = async (
  cupixApi: CupixApi,
  aerialMapId: number,
  report: IReportInfo,
  calibration: ICalibrations,
  aerialPhotos: IAerialPhoto[],
) => {
  for (let i = 0; i < aerialPhotos.length; i++) {
    const aerialPhoto = aerialPhotos[i];
    const aerialPhotoUploadCredential = await cupixApi.createAerialPhotoS3UploadCredential(aerialMapId, aerialPhoto.id);

    const imageError = report.imageErrors[aerialPhoto.id];
    logger.debug(`[CupixAerialMap] aerial photo(${aerialPhoto.id}) imageError: ${JSON.stringify(imageError)}`);
    const cameraCalibration = calibration[aerialPhoto.id];
    logger.debug(
      `[CupixAerialMap] aerial photo(${aerialPhoto.id}) upload bucket: ${aerialPhotoUploadCredential.bucket}, path: ${aerialPhotoUploadCredential.basepath}`,
    );
    const stream = await downloadStreamByUrl(aerialPhoto.downloadUrl);  // downloadUrl이 null이면 ERR_INVALID_URL
  • Failure point: common/s3.ts:164-166downloadStreamByUrlnull을 URL로 사용
common/s3.ts:164-166typescript
export const downloadStreamByUrl = async (downloadUrl: string) => {
  const response = await axios({ method: 'get', url: downloadUrl, responseType: 'stream' });
  return response.data;
};
  • API 응답 매핑: common/api.ts:1024-1035download_urlnull이어도 TypeScript 타입 검증 없이 그대로 매핑
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,  // null이 그대로 전달됨
    capturedAt: photo.attributes.captured_at,
    longitude: photo.attributes.longitude,
    latitude: photo.attributes.latitude,
    altitude: photo.attributes.altitude,
  };
});
  • 타입 정의에서 download_urldownloadUrlstring으로 선언되어 있지만, 런타임에서 API가 null을 반환할 수 있다:
type/type.ts:159-183typescript
export interface IAerialPhotoData {
  id: string;
  type: string;
  attributes: {
    id: number;
    name: string;
    state: string;
    download_url: string;  // 실제로는 string | null
    // ...
  };
}

export interface IAerialPhoto {
  // ...
  downloadUrl: string;  // 실제로는 string | null
  // ...
}

기대 동작: API에서 모든 aerial photo에 유효한 download_url을 반환하고, downloadStreamByUrl이 정상적으로 이미지를 다운로드한다.

실제 동작: 특정 aerial photo의 download_urlnull로 반환되어, downloadStreamByUrl("null")axios({ url: "null" }) → Node.js URL 파서가 new URL("null") 시도 → TypeError [ERR_INVALID_URL] 발생.

Log Evidence#

사용한 Datadog 쿼리:

text
service:aerial-map-service status:error "ERR_INVALID_URL"
Time range: 2026-04-23T06:00:00Z to 2026-04-23T08:30:00Z

핵심 에러 로그:

json
{
  "timestamp": "2026-04-23T07:35:06Z",
  "status": "error",
  "message": "[CupixAerialMap] postprocess fail - error:({\"input\":\"null\",\"code\":\"ERR_INVALID_URL\"}) / message:(Invalid URL)"
}

error.input 값이 "null" (문자열)인 것이 핵심 증거이다. JavaScript에서 null 값이 문자열 컨텍스트에서 "null"로 변환된 것을 보여준다.

동일 시간대 postprocess 시작 로그:

text
service:aerial-map-service "postprocess"
Time range: 2026-04-23T07:30:00Z to 2026-04-23T07:40:00Z
json
{
  "timestamp": "2026-04-23T07:32:58Z",
  "message": "[CupixAerialMap] postprocess fargate memory limit: 8192 MB"
}
json
{
  "timestamp": "2026-04-23T07:33:00Z",
  "message": "[CupixAerialMap] postprocess - pix4d output:({\"pix4d_log\":{...},\"orthomosaic\":{...},\"dsm\":{...},\"mesh\":{...},\"flight_path\":{...},\"thumbnail\":{...},\"report_pdf\":{...},\"report_json\":{...},\"scene_ref_frame\":{...}})"
}

Pix4D output 조회는 정상적으로 완료되었으며, 이후 processAerialPhoto 단계에서 실패한 것을 타임라인으로 확인할 수 있다. 또한 Step Functions에서 ECS 태스크의 ExitCode: 1, StopCode: EssentialContainerExited를 기록했다:

json
{
  "timestamp": "2026-04-23T07:36:03Z",
  "message": "[CupixAerialMap] cupix-postprocess-check-production-vdco - cause(undefined) / environment({\"Name\":\"INPUT_DATA\",\"Value\":\"{\\\"result\\\":\\\"success\\\",\\\"aerialMap\\\":{\\\"id\\\":335,...}}\"})"
}

같은 날 동일 서비스에서 504 Gateway Timeout 에러도 다수 발생했으나, 이는 별도의 CloudFront 연결 문제이며 본 ERR_INVALID_URL 에러와는 직접적 관련이 없다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Aerial photo의 download_urlnull로 반환되어 URL 파싱 실패 에러 메시지에 input: "null" 명시. api.ts:1029에서 null 검증 없이 매핑. process.ts:66에서 null 검증 없이 downloadStreamByUrl 호출 Confirmed
H2 CloudFront 504 Timeout으로 인해 API 응답이 비정상적으로 반환됨 같은 날 504 Gateway Timeout 에러 다수 발생 (15:28~16:04 KST) ERR_INVALID_URL 에러의 input 값이 "null"이며, 이는 timeout 응답과 무관한 null 값 문제. 504 에러는 다른 aerial map의 preprocess/postprocess에서 발생한 별도 이슈 Rejected
H3 Pix4D 처리 실패로 인해 aerial photo 데이터가 불완전하게 반환됨 Step Functions 로그에서 result: "success" 확인. Pix4D output 조회도 모든 output을 포함하여 정상 반환됨 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • postprocess/process.ts:66processAerialPhoto 함수에서 aerialPhoto.downloadUrl이 유효한 URL인지 검증한 후 downloadStreamByUrl을 호출해야 한다. downloadUrl이 null/undefined인 aerial photo는 skip하고 warning 로그를 남기는 것이 적절하다.

단기 개선 (1주 이내)#

  • type/type.ts:166,178download_urldownloadUrl 타입을 string | null로 변경하여 컴파일 타임에 null 체크를 강제해야 한다.
  • common/api.ts:1024-1035getAerialPhotosByAerialMapId 응답 매핑에서 download_url이 null인 photo를 필터링하거나, null일 경우 명시적으로 처리하는 로직 추가.
  • common/s3.ts:164downloadStreamByUrl 함수에 defensive URL 검증 추가 (null/undefined/빈 문자열 체크).

장기 개선 (재발 방지)#

  • Cupix API 응답의 nullable 필드에 대해 TypeScript 타입을 실제 API 스펙과 일치시키는 전반적인 타입 감사 수행. 현재 여러 필드가 string으로 선언되어 있지만 실제로는 null을 반환할 수 있음.
  • download_url이 null인 이유를 Cupix API 측에서 조사 — aerial photo가 아직 업로드 완료되지 않았거나, presigned URL 생성에 실패한 경우일 수 있음.

Monitoring#

  • downloadUrl이 null인 aerial photo 발생 시 warn 레벨 로그 추가
  • Datadog 쿼리:
text
service:aerial-map-service "ERR_INVALID_URL"
text
service:aerial-map-service "download_url is null"

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: trivial — null 검증 로직만 추가하면 되며, 핵심 비즈니스 로직 변경은 불필요하다. 다만 download_url이 null인 aerial photo에 대한 비즈니스 정책 결정이 필요하다 (skip vs retry vs fail).