[CupixAerialMap] postprocess fail Invalid URL
RCA: [CupixAerialMap] postprocess fail Invalid URL
Overview#
What Happened#
2026-04-23 16:35 KST, aerial-map-service의 postprocess ECS Fargate 컨테이너에서 aerial map ID 335에 대한 후처리 작업 중 ERR_INVALID_URL 에러가 발생하여 컨테이너가 exit code 1로 종료되었다. Cupix API의 createAerialMapS3UploadCredential 응답에서 endpoint 필드가 null로 반환되었고, 이 값이 AWS S3 SDK v3 S3Client 생성자에 그대로 전달되면서 내부적으로 new URL(null) → new URL("null")이 호출되어 실패했다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | TypeError [ERR_INVALID_URL] |
| exception.message | Invalid URL |
| top_frame | @aws-sdk/client-s3 S3Client constructor (endpoint resolution) |
| runtime | Node.js 20, ECS Fargate |
| env | production, us-west-2 |
Timeline#
- 16:35:06 KST — postprocess ECS 컨테이너 시작 (aerial map 335, Step Functions에 의해 트리거)
- 16:35:06 KST —
ERR_INVALID_URL발생, 컨테이너 exit code 1로 종료 - 16:36:03 KST —
postprocess-checkLambda가 ECS task 실패 감지 - 16:36:05 KST —
process-failLambda 실행, aerial map 335 상태를 fail로 업데이트 - 16:36:06 KST —
data-collectLambda 실행, aerial map 데이터 수집 - 2026-04-23 — error-sweeper가 에러 클러스터 감지, RCA 수행
Error Log#
[CupixAerialMap] postprocess fail Invalid URL
Impact#
- Service:
aerial-map-service - 발생 횟수: 클러스터 내 1회 (동일 날짜 18:48에 동일 에러 재발 확인)
- 최초 발생: 2026-04-23T07:35:06.252Z
- 최근 발생: 2026-04-23T07:35:06.252Z
- 영향: aerial map 335의 postprocess 실패로 인해 해당 aerial map의 orthomosaic, mesh, DSM 타일링 및 업로드가 완료되지 못함. 사용자에게 처리된 결과물이 제공되지 않음.
Root Cause Summary#
Cupix API의 createAerialMapS3UploadCredential 엔드포인트(/api/v1/aerial_maps/{id}/upload_credentials)가 응답 JSON에 endpoint: null을 반환한다. aerial-map-service의 s3.ts에서 이 값을 AWS SDK v3 S3Client 생성자의 endpoint 옵션으로 그대로 전달하는데, SDK 내부에서 endpoint가 null이면 이를 문자열 "null"로 coerce한 뒤 new URL("null")을 호출하여 ERR_INVALID_URL이 발생한다. postprocess 흐름 초기에 S3 업로드 credential을 요청하자마자 실패하므로, 모든 후처리 작업(타일링, 업로드 등)이 중단된다.
Technical Analysis#
Code Path#
- Entry point:
postprocess/index.ts:172— 비동기 IIFE가app(input)을 호출 - CupixApi 초기화:
postprocess/index.ts:56-58—CupixApi인스턴스 생성, access token 설정 - 첫 API 호출:
postprocess/index.ts:59-63—saveProcessingState및getAerialMap호출 (axios HTTP, 성공) - Pix4D API 호출:
postprocess/index.ts:67-70— Pix4D access token, S3 credential, processing output 조회 (성공) - Report 처리:
postprocess/process.ts:122-148— report 다운로드 후 upload credential 요청 - Failure point:
common/api.ts:870-896→common/s3.ts:40-73—createAerialMapS3UploadCredential가endpoint: null을 반환하고, 이것이uploadObjectByKey의S3Client생성자에 전달됨
async createAerialMapS3UploadCredential(aerialMapId: number): Promise<IOutputUploadAndDownloadCredential> {
try {
const response = await axios.post(
`${this._endpoint}/api/v1/aerial_maps/${aerialMapId}/upload_credentials?fields=id,key`,
{},
{ headers: { 'x-cupix-auth': `${this._accessToken}` } },
);
return {
region: response.data.result['bucket_region'],
bucket: response.data.result['bucket_name'],
basepath: response.data.result['basepath'],
accessKey: response.data.result['aws_access_key_id'],
secretKey: response.data.result['aws_secret_access_key'],
sessionToken: response.data.result['aws_session_token'],
endpoint: response.data.result['endpoint'], // API가 null을 반환
};
} catch (error: any) { ... }
}
endpoint 필드가 null로 반환되면 credential 객체에 endpoint: null이 설정된다.
export const uploadFile = async (key: string, filePath: string, credential: IOutputUploadAndDownloadCredential) => {
const { region, bucket, basepath, accessKey, secretKey, sessionToken, endpoint } = credential;
await uploadObjectByKey(
{
region: region,
bucket: bucket,
key: `${basepath}/${key}`,
endpoint: endpoint, // null이 그대로 전달됨
},
{ ... },
filePath,
);
};
export const uploadObjectByKey = async (
bucket: IS3Bucket,
credential: IS3Credential,
filePath: string,
): Promise<void> => {
const uploadS3 = new S3Client({
region: bucket.region,
credentials: { ... },
maxAttempts: 5,
retryMode: 'adaptive',
forcePathStyle: true,
endpoint: bucket.endpoint, // null → SDK 내부에서 new URL("null") 호출 → ERR_INVALID_URL
});
AWS SDK v3의 S3Client는 endpoint 옵션이 undefined면 무시하지만, null이면 이를 문자열로 coerce하여 new URL("null")을 호출한다. Node.js에서 new URL("null")은 TypeError [ERR_INVALID_URL]: Invalid URL를 발생시킨다.
Log Evidence#
Datadog에서 확인한 에러 상세 로그:
service:aerial-map-service status:error "postprocess fail"
에러 발생 시 두 개의 로그가 기록된다:
{
"timestamp": "2026-04-23 16:35:06 KST",
"status": "error",
"message": "[CupixAerialMap] postprocess fail - error:({\"input\":\"null\",\"code\":\"ERR_INVALID_URL\"}) / message:(Invalid URL)"
}
{
"timestamp": "2026-04-23 16:35:06 KST",
"status": "error",
"message": "[CupixAerialMap] postprocess fail Invalid URL"
}
error 객체의 input 필드가 "null" (문자열)이고 code가 ERR_INVALID_URL인 것은 Node.js의 new URL("null") 호출 결과와 정확히 일치한다.
동일 에러가 같은 날 18:48:54 KST에 재발:
{
"timestamp": "2026-04-23 18:48:54 KST",
"status": "error",
"message": "[CupixAerialMap] postprocess fail - error:({\"input\":\"null\",\"code\":\"ERR_INVALID_URL\"}) / message:(Invalid URL)"
}
postprocess-check Lambda가 ECS task 실패를 감지한 로그에서 aerial map 335의 처리 컨텍스트 확인:
service:aerial-map-service "postprocess-check"
{
"timestamp": "2026-04-23 16:36:03 KST",
"message": "[CupixAerialMap] cupix-postprocess-check-production-vdco - cause(undefined) / environment({\"Name\":\"INPUT_DATA\",\"Value\":\"{...\\\"aerialMap\\\":{\\\"id\\\":335,...}\"}) / value({\"result\":\"success\",\"aerialMap\":{\"id\":335,\"processing_option\":{\"method\":\"nadir\",\"outputs\":[\"orthomosaic\",\"mesh\",\"dsm\"],\"resolution\":\"high\"}},\"session\":{\"id\":10433064,\"token\":\"vljuot9w2bwh\"}})"
}
ECS task 메타데이터에서 컨테이너가 ExitCode: 1로 종료되고 StopCode: EssentialContainerExited임을 확인:
"ExitCode":1,"StopCode":"EssentialContainerExited","StoppedReason":"Essential container in task exited"
또한, 같은 시간대에 504 Gateway Timeout 에러가 다수 발생하고 있어 Cupix API CloudFront가 간헐적 불안정:
service:aerial-map-service status:error "504 Gateway Timeout"
[CupixAerialMap] postprocess fail "<!DOCTYPE HTML...504 Gateway Timeout ERROR...</HTML>"
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Cupix API upload_credentials 응답의 endpoint 필드가 null로 반환되어 S3Client에서 new URL("null") 호출 실패 |
에러 객체 {"input":"null","code":"ERR_INVALID_URL"} — input이 문자열 "null"이므로 JS의 null → "null" coercion 패턴과 일치. api.ts:888에서 endpoint: response.data.result['endpoint']를 무조건 할당. s3.ts:55에서 endpoint: bucket.endpoint를 S3Client에 전달 |
— | Confirmed |
| H2 | CUPIX_ENDPOINT 환경변수 미설정으로 API URL 자체가 잘못됨 |
api.ts:560에서 process.env.CUPIX_ENDPOINT || '' 사용 |
ECS task definition (ecs/main.tf:140-141)에 CUPIX_ENDPOINT 명시적으로 설정됨. saveProcessingState/getAerialMap API 호출이 먼저 성공한 것으로 보아 endpoint는 유효함 |
Rejected |
| H3 | PIX4D_ENDPOINT 미설정으로 Pix4D API 호출 실패 |
api.ts:48에서 process.env.PIX4D_ENDPOINT || '' 사용 |
ECS task definition (ecs/main.tf:205-206)에 PIX4D_ENDPOINT 설정됨. 에러 메시지가 Pix4D 관련이 아닌 URL parsing 에러 |
Rejected |
| H4 | S3 download credential의 endpoint 문제 (Pix4D S3 credential) | Pix4D S3 credential도 endpoint를 포함할 수 있음 | getPix4dS3Credential (api.ts:99-122)의 반환 타입에 endpoint 필드 없음 — key만 포함. download 시 s3.ts:16-25의 downloadObjectByKey는 Pix4D credential 사용, endpoint: bucket.endpoint는 ISource의 일부로 endpoint 필드 자체가 없음 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
s3.ts:55—S3Client생성 시endpoint옵션에null/falsy 값이 전달되지 않도록 방어 코드 추가.endpoint: bucket.endpoint || undefined로 변경하면null이undefined로 변환되어 SDK가 기본 endpoint를 사용한다.api.ts:888—createAerialMapS3UploadCredential에서endpoint: response.data.result['endpoint'] || undefined로 변경하여 source에서 null을 제거.- 동일 패턴이
createAerialPhotoS3UploadCredential(api.ts:919)에도 존재하므로 함께 수정.
단기 개선 (1주 이내)#
uploadObjectByKey,uploadObjectsByKey,uploadS3StreamByKey등 S3Client를 생성하는 모든 함수에서endpoint값에 대한 null-safe 처리를 일관되게 적용.IOutputUploadAndDownloadCredential타입의endpoint필드를endpoint?: string | undefined로 명시하고null이 할당되지 않도록 타입 가드 추가.
장기 개선 (재발 방지)#
- Cupix API 측에서
upload_credentials응답의endpoint필드가null인 경우 해당 필드를 응답에서 제외하거나, 유효한 S3 endpoint URL을 반환하도록 수정. aerial-map-service의 S3 관련 유틸리티를 중앙화하여 S3Client 생성 로직을 단일 factory 함수로 통합하고, endpoint validation을 한 곳에서 수행.
Monitoring#
- 추가 알림:
ERR_INVALID_URL에러 발생 시 즉시 알림
service:aerial-map-service status:error "ERR_INVALID_URL"
- S3 업로드 실패율 모니터링:
service:aerial-map-service status:error "upload"
Risk Assessment#
- Risk level: medium — 동일 에러가 같은 날 2회 발생했으며, Cupix API가
endpoint: null을 반환하는 한 모든 postprocess 작업이 실패할 수 있음 - 예상 복잡도: trivial —
|| undefined또는?? undefined한 줄 변경으로 수정 가능