[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_url이 null로 반환되어 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#
- 2026-04-23T07:32:58Z — Postprocess Fargate 태스크 시작 (memory limit: 8192 MB)
- 2026-04-23T07:33:00Z — Pix4D output 조회 성공 (orthomosaic, mesh, dsm, flight_path 등)
- 2026-04-23T07:35:06Z —
processAerialPhoto에서downloadStreamByUrl(null)호출 시ERR_INVALID_URL발생 - 2026-04-23T07:35:06Z — Postprocess fail 로깅 후 exit code 1로 종료
- 2026-04-23T07:36:03Z — Step Functions postprocess-check가
States.TaskFailed감지
Error Log#
[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)을 호출하는데, downloadUrl이 null인 경우에 대한 검증이 없다. 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:71—getAerialPhotosByAerialMapId(aerialMapId)호출하여 aerial photo 목록 조회postprocess/process.ts:56-66—processAerialPhoto에서 각 aerial photo를 순회하며 thumbnail 생성
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-166—downloadStreamByUrl이null을 URL로 사용
export const downloadStreamByUrl = async (downloadUrl: string) => {
const response = await axios({ method: 'get', url: downloadUrl, responseType: 'stream' });
return response.data;
};
- API 응답 매핑:
common/api.ts:1024-1035—download_url이null이어도 TypeScript 타입 검증 없이 그대로 매핑
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_url과downloadUrl은string으로 선언되어 있지만, 런타임에서 API가null을 반환할 수 있다:
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_url이 null로 반환되어, downloadStreamByUrl("null") → axios({ url: "null" }) → Node.js URL 파서가 new URL("null") 시도 → TypeError [ERR_INVALID_URL] 발생.
Log Evidence#
사용한 Datadog 쿼리:
service:aerial-map-service status:error "ERR_INVALID_URL"
Time range: 2026-04-23T06:00:00Z to 2026-04-23T08:30:00Z
핵심 에러 로그:
{
"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 시작 로그:
service:aerial-map-service "postprocess"
Time range: 2026-04-23T07:30:00Z to 2026-04-23T07:40:00Z
{
"timestamp": "2026-04-23T07:32:58Z",
"message": "[CupixAerialMap] postprocess fargate memory limit: 8192 MB"
}
{
"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를 기록했다:
{
"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_url이 null로 반환되어 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:66—processAerialPhoto함수에서aerialPhoto.downloadUrl이 유효한 URL인지 검증한 후downloadStreamByUrl을 호출해야 한다.downloadUrl이 null/undefined인 aerial photo는 skip하고 warning 로그를 남기는 것이 적절하다.
단기 개선 (1주 이내)#
type/type.ts:166,178—download_url과downloadUrl타입을string | null로 변경하여 컴파일 타임에 null 체크를 강제해야 한다.common/api.ts:1024-1035—getAerialPhotosByAerialMapId응답 매핑에서download_url이 null인 photo를 필터링하거나, null일 경우 명시적으로 처리하는 로직 추가.common/s3.ts:164—downloadStreamByUrl함수에 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 쿼리:
service:aerial-map-service "ERR_INVALID_URL"
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).