ES /docs

[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#

  1. 16:35:06 KST — postprocess ECS 컨테이너 시작 (aerial map 335, Step Functions에 의해 트리거)
  2. 16:35:06 KSTERR_INVALID_URL 발생, 컨테이너 exit code 1로 종료
  3. 16:36:03 KSTpostprocess-check Lambda가 ECS task 실패 감지
  4. 16:36:05 KSTprocess-fail Lambda 실행, aerial map 335 상태를 fail로 업데이트
  5. 16:36:06 KSTdata-collect Lambda 실행, aerial map 데이터 수집
  6. 2026-04-23 — error-sweeper가 에러 클러스터 감지, RCA 수행

Error Log#

Datadog Logs

text
[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-services3.ts에서 이 값을 AWS SDK v3 S3Client 생성자의 endpoint 옵션으로 그대로 전달하는데, SDK 내부에서 endpointnull이면 이를 문자열 "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-58CupixApi 인스턴스 생성, access token 설정
  • 첫 API 호출: postprocess/index.ts:59-63saveProcessingStategetAerialMap 호출 (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-896common/s3.ts:40-73createAerialMapS3UploadCredentialendpoint: null을 반환하고, 이것이 uploadObjectByKeyS3Client 생성자에 전달됨
src/code/src/common/api.ts:870-896typescript
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이 설정된다.

src/code/src/postprocess/upload.ts:7-28typescript
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,
  );
};
src/code/src/common/s3.ts:40-56typescript
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의 S3Clientendpoint 옵션이 undefined면 무시하지만, null이면 이를 문자열로 coerce하여 new URL("null")을 호출한다. Node.js에서 new URL("null")TypeError [ERR_INVALID_URL]: Invalid URL를 발생시킨다.

Log Evidence#

Datadog에서 확인한 에러 상세 로그:

text
service:aerial-map-service status:error "postprocess fail"

에러 발생 시 두 개의 로그가 기록된다:

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

error 객체의 input 필드가 "null" (문자열)이고 codeERR_INVALID_URL인 것은 Node.js의 new URL("null") 호출 결과와 정확히 일치한다.

동일 에러가 같은 날 18:48:54 KST에 재발:

json
{
  "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의 처리 컨텍스트 확인:

text
service:aerial-map-service "postprocess-check"
json
{
  "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임을 확인:

text
"ExitCode":1,"StopCode":"EssentialContainerExited","StoppedReason":"Essential container in task exited"

또한, 같은 시간대에 504 Gateway Timeout 에러가 다수 발생하고 있어 Cupix API CloudFront가 간헐적 불안정:

text
service:aerial-map-service status:error "504 Gateway Timeout"
text
[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-25downloadObjectByKey는 Pix4D credential 사용, endpoint: bucket.endpointISource의 일부로 endpoint 필드 자체가 없음 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • s3.ts:55S3Client 생성 시 endpoint 옵션에 null/falsy 값이 전달되지 않도록 방어 코드 추가. endpoint: bucket.endpoint || undefined로 변경하면 nullundefined로 변환되어 SDK가 기본 endpoint를 사용한다.
  • api.ts:888createAerialMapS3UploadCredential에서 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 에러 발생 시 즉시 알림
text
service:aerial-map-service status:error "ERR_INVALID_URL"
  • S3 업로드 실패율 모니터링:
text
service:aerial-map-service status:error "upload"

Risk Assessment#

  • Risk level: medium — 동일 에러가 같은 날 2회 발생했으며, Cupix API가 endpoint: null을 반환하는 한 모든 postprocess 작업이 실패할 수 있음
  • 예상 복잡도: trivial — || undefined 또는 ?? undefined 한 줄 변경으로 수정 가능