ES /docs

SYSTEM - Uncaught Exception: Cannot read properties of undefined (reading 'statusCode')

RCA: SYSTEM - Uncaught Exception: Cannot read properties of undefined (reading 'statusCode')

Overview#

What Happened#

cupixworks-capture-singleshot-agent (production, us-west-2) 에서 stitched pano 업로드 직후 request 콜백이 network error 를 받았는데도 응답 객체가 undefined 인 채로 response.statusCode 를 참조하면서 uncaught exception 이 발생했다. 에러 직전 같은 컨테이너에서 ETIMEDOUT (syscall:"read") 와 SingleshotService::uploadStitchedPano | image is not uploaded 가 기록됐다. 1회 발생 후 process 가 exit code 1 로 종료되었으며 status board 가 같은 서비스의 다른 cluster (8803d72d-…) 와 묶어 임시 incident 를 열었다가 곧바로 resolved 처리했다.

Quick Facts#

Field Value
exception.class TypeError
exception.message Cannot read properties of undefined (reading 'statusCode')
top_frame packages/api/src/api/pano.api.ts:170 (request callback inside uploadImage)
triggering call SingleshotService::uploadStitchedPanocupixApi.pano.uploadImage
env production, region us-west-2
tenant cupix

Affected Teams#

Team / Domain Error Count Impact
capture / singleshot-agent (chrischoi) 1 (cluster), 추가 동시 cluster 1건 (8803d72d-…) Stitched pano upload 실패 → singleshot capture 가 Stopped 상태로 정상 마감되지 못하고 worker 프로세스가 강제 종료. 같은 SQS 메시지가 visibility timeout 후 재시도되지만 동일 transient network error 발생 시 동일하게 crash 가능.

Timeline#

  1. 2026-06-19 11:43:34 KST 부터CupixAuth::handleError | Response statusCode: 404, requestUriHref: …/api/v1/sessions/sagemaker_invoke_credentials 가 약 10분 간 반복 (transient session/credential 문제 — 이번 root cause 와는 별개의 backround warning).
  2. 2026-06-19 11:53:12 KSTBaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETIMEDOUT","syscall":"read"}, ...} (직전 SQS 메시지 처리 중 read timeout).
  3. 2026-06-19 11:53:27 KSTSingleshotService::uploadStitchedPano | image is not uploaded (cluster 8803d72d-… 와 같은 시점).
  4. 2026-06-19 11:54:09 KST (= 02:54:09 UTC, cluster last_seen)SYSTEM - Uncaught Exception: Cannot read properties of undefined (reading 'statusCode') 발생, agent process exit.
  5. 2026-06-19 11:54:09 KSTChildProcessManager::setupEventHandlers | Child process exited / Child process closed 가 연쇄적으로 기록됨 (동일/후속 컨테이너의 child process 정리).
  6. 2026-06-19 12:16:02 KST — 후속 메시지 재시도에서 정상 path 의 BaseService::handlingMessageErrors (statusCode 400, RESC10000 Resource does not uploaded) 가 기록됨 (정상 에러 핸들링 — uncaught exception 재현 안 됨).

Error Log#

Datadog Logs

text
SYSTEM - Uncaught Exception: Cannot read properties of undefined (reading 'statusCode')

Impact#

  • Service: cupixworks-capture-singleshot-agent
  • Team: chrischoi
  • 발생 횟수: 1
  • 최초 발생: 2026-06-19 11:54 KST
  • 최근 발생: 2026-06-19 11:54 KST

여러 message 동시 처리하던 ECS task 가 SIGABRT 수준 영향: process.on('uncaughtException') 핸들러가 process.exit(1) 을 호출하여 즉시 종료된다. 진행 중이던 SQS 메시지는 ack 되지 않으므로 visibility timeout 이후 재처리되지만, 같은 네트워크 환경에서 다시 transient error 가 발생하면 동일 crash 가 반복될 수 있다.

Root Cause Summary#

Status update (Revision 1): 본 결함은 origin/develop 의 commit 567dd4802 (TSLA-13233, 2026-06-22) 으로 수정 완료됐다 — uploadImage 가 axios 기반 공통 uploadFile 로 통합되어 request callback 의 undefined response 경로가 제거됨. 자세한 매핑은 아래 Fix Recommendation 및 Revision History 참고.

packages/api/src/api/pano.api.ts:154-177uploadImage 함수가 request(options, (error, response, body) => …) 콜백에서 error 를 무시하고 response.statusCode 에 직접 접근한다. S3 PUT (또는 그와 동등한 upload URL) 요청이 network error 로 실패하면 request 콜백 규약상 error 만 채워지고 responseundefined 가 된다. 이 경우 response.statusCode == 200 비교에서 TypeError: Cannot read properties of undefined (reading 'statusCode') 가 throw 된다. 콜백은 Promise executor 내부지만 reject 로 잡히지 않으므로 exception 이 microtask queue 를 빠져나와 process 의 uncaughtException 핸들러로 흘러가고, 핸들러가 process.exit(1) 을 호출하여 agent 가 종료된다. 직전 11:53:12 KST 에 errno:-110 / code:"ETIMEDOUT" / syscall:"read" 가 기록되었고, 11:53:27 KST 에 image is not uploaded 가 기록된 것이 동일 transient network 조건이 uploadImage 콜백을 trigger 했음을 뒷받침한다.

Technical Analysis#

Code Path#

  • Entry point: packages/cupix-tesla-singleshot-agent/src/app.ts:39new SingleshotService().init()
  • SQS message loop: packages/base/src/base-service.ts:86-116 (checkingQueuerunByMessagesrunByMessage)
  • Per-capture run: packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:37-45runuploadStitchedPano(targetId) 를 await
  • Upload step: packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:122await this.cupixApi.pano.uploadImage(stitchedUploadUrl, stitchedImagePath)
  • Failure point: packages/api/src/api/pano.api.ts:170if (response.statusCode == 200) { ... } (response 가 undefined 일 때 TypeError)
  • Process-level handler: packages/cupix-tesla-singleshot-agent/src/app.ts:27-31process.on('uncaughtException', …) 가 메시지를 로깅하고 process.exit(1) 호출
packages/api/src/api/pano.api.ts:154-177typescript
uploadImage = (url: string, filePath: string): Promise<boolean> => {
    const fileStream = fs.createReadStream(filePath);
    const fileStats = fs.statSync(filePath);

    const options = {
        method: 'PUT',
        url: url,
        body: fileStream,
        headers: {
            'Content-Type': 'binary/octet-stream',
            'Content-Length': fileStats.size
        }
    };

    return new Promise((resolve, reject) => {
        request(options, (error, response, body) => {
            if (response.statusCode == 200) {   // ← response 가 undefined 면 TypeError
                resolve(true);
            }

            resolve(false);
        });
    });
};

기대 동작: network/transport error 가 발생하면 error 가 채워지고 responseundefined 가 될 수 있으므로, error 를 먼저 검사하여 resolve(false) 또는 reject(error) 하고, 그 후 response?.statusCode === 200 처럼 안전하게 접근해야 한다. 실제 동작: error 분기를 무시하고 response.statusCode 를 직접 읽어 TypeError 가 발생한다.

상위 호출자는 이미 false 반환에 대비한 분기를 가지고 있어 정상 실패 시에는 graceful 하게 빠져나간다:

packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:122-127typescript
const isUploaded = await this.cupixApi.pano.uploadImage(stitchedUploadUrl, stitchedImagePath);

if (!isUploaded) {
    logger.error('SingleshotService::uploadStitchedPano | image is not uploaded');
    return;
}

따라서 uploadImage 내부에서 throw 만 하지 않으면 cluster 8803d72d-…image is not uploaded 처럼 정상 에러 경로로 흡수된다. uncaughtException 은 콜백에서 직접 throw 된 TypeError 가 Promise reject 로 잡히지 않은 것이 본질이다.

uncaughtException 핸들러는 다음과 같이 즉시 process 종료를 일으킨다:

packages/cupix-tesla-singleshot-agent/src/app.ts:27-31typescript
process.on('uncaughtException', (error: any) => {
    console.error('SYSTEM - Uncaught Exception:', error);
    logger.error('SYSTEM - Uncaught Exception: %s', error instanceof Error ? error.message : JSON.stringify(error));
    exitHandler({ exit: true }, 1);
});

exitHandler({ exit: true }, 1)process.exit() 으로 인해 진행 중이던 SQS 메시지는 ack 되지 않은 상태로 task 가 죽는다. 같은 패턴의 핸들러가 cupix-capture-intelligence-agent, cupix-tesla-room-agent, cupix-pano-postprocessor 등 다른 agent 들에도 동일하게 존재한다 (Grep 결과 참고).

Log Evidence#

Datadog query (cluster 파일에서 그대로 사용):

text
service:cupixworks-capture-singleshot-agent status:error @environment:production "SYSTEM - Uncaught Exception: Cannot read properties of undefined (reading 'statusCode')"

추가로 사용한 query:

text
service:cupixworks-capture-singleshot-agent status:error
service:cupixworks-capture-singleshot-agent "statusCode"

핵심 로그 (시간순, KST):

text
2026-06-19 11:53:12 KST  error  BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETIMEDOUT","syscall":"read"},"sqsMessage":{"MessageId":"d8971086-723a-40db-9bb2-2272647b9dfe","Attributes":{"ApproximateReceiveCount":"1"}}}
2026-06-19 11:53:27 KST  error  SingleshotService::uploadStitchedPano | image is not uploaded
2026-06-19 11:54:09 KST  error  SYSTEM - Uncaught Exception: Cannot read properties of undefined (reading 'statusCode')
2026-06-19 11:54:09 KST  error  ChildProcessManager::setupEventHandlers | Child process exited
2026-06-19 11:54:09 KST  error  ChildProcessManager::setupEventHandlers | Child process closed

비교용 — 같은 서비스의 정상 에러 처리 (uncaught 가 아님): API 가 명시적으로 statusCode 를 돌려준 경우 getApiErrorToDeleteMessage 가 statusCode 를 정상적으로 다룬다.

text
2026-06-19 12:16:02 KST  warn   BaseService::getApiErrorToDeleteMessage | error msg - {"statusCode":400,"requestUriHref":"http://api-tesla.cupix.internal/api/v1/panos/89260887/check_uploading?...","bodyResult":{"code":"RESC10000","type":"Cupix::Errors::Resource","reason":"Resource does not uploaded","message":"Resource does not uploaded"},"modelId":718445}

이 11:53:12 KST 의 ETIMEDOUT 는 명백한 transport-layer error (response 객체 없이 error 만 전달) 이며, getApiErrorToDeleteMessage 가 245-247 라인에서 errno/code/syscall 조합을 인지하여 undefined 를 반환하는 처리를 가지고 있다 — 즉 codebase 가 transport error 시 response 가 없을 수 있다는 사실을 이미 다른 곳에서는 인식하고 있다. 하지만 uploadImage 콜백은 그 인식이 빠져 있다.

Status board 결과:

json
{
  "scope": "svc:cupixworks-capture-singleshot-agent",
  "active": null,
  "recent": [{
    "id": "2026-06-19-svc-cupixworks-capture-singleshot-agent-1",
    "title": "cupixworks-capture-singleshot-agent service degraded",
    "status": "resolved",
    "cluster_ids": ["8803d72d-3509-44a6-8012-7b2cc32fcacd", "6e7c4f07-4010-431f-bf2f-5ea59189310b"]
  }]
}

같은 시간대 cluster 8803d72d-… 가 함께 묶여 있어, image is not uploadedstatusCode TypeError 가 동일 incident 내부의 두 표현임을 확인.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 uploadImage 콜백이 transport error 시 responseundefined 인데도 response.statusCode 에 접근하여 TypeError 발생, 이 TypeError 가 Promise executor 내부의 callback 스택에서 throw 되어 unhandled 가 됨 pano.api.ts:170 의 코드 (if (response.statusCode == 200) — error/response undefined 분기 없음); 11:53:12 KST 의 ETIMEDOUT errno:-110 syscall:"read" 로그; 직후 11:54:09 KST 의 uncaughtException; 같은 서비스의 다른 cluster 8803d72d-…image is not uploaded 로 분기 — 즉 정상 path 는 이미 false 반환을 처리 가능 Confirmed
H2 sagemaker session credentials 404 (`CupixAuth::handleError Response statusCode: 404, ... sagemaker_invoke_credentials`) 가 실제 root cause 11:43–11:54 KST 사이 동일 404 가 약 10회 반복됨 이는 warn 레벨이고 CupixAuth::handleError 가 statusCode 를 정상적으로 다루는 path 임 (코드/로그 모두에서 response 객체가 존재). Cannot read properties of undefined 는 이 path 에서 throw 되지 않음
H3 외부 dependency (S3 등) 장애로 인한 incident — RCA 불필요 status-board 가 동일 서비스의 두 cluster 를 묶어 incident 를 잠시 열었음 scope 가 svc:cupixworks-capture-singleshot-agent 로 internal service 이며 active 가 아닌 resolved (자동). 외부 dep scope (dep:s3-…) 아님. 두 cluster 모두 이 코드 결함의 두 표현 Rejected
H4 align.module.ts:358downloadFileres.statusCode 가 원인 같은 단어 statusCode 를 다룸 해당 함수는 sendReq.on('response') 콜백을 사용하는데, response 이벤트가 발생하면 res 는 정의된 IncomingMessage 객체이다. error 시에는 'error' 이벤트로 가서 reject 되므로 undefined 접근 불가. 또한 runAlignScriptawait 으로 감싸 정상 reject 처리됨 Rejected

Fix Recommendation#

Status: Already resolved on origin/develop#

Revision 1 조사 결과, 본 RCA 의 즉시/단기/장기 권장사항이 모두 commit 567dd4802 (TSLA-13233, "feat: unify S3/signed-URL uploads onto @agents/base retry infra", 2026-06-22 dominik.oh) 으로 origin/develop 에 이미 반영됨. 아래는 권장사항 vs 실제 반영의 매핑이다.

  • 즉시 조치 (uploadImage 의 undefined response 분기 처리)packages/api/src/api/pano.api.tsuploadImagePromise<boolean> callback 구현에서 Promise<void> async wrapper 로 변경되었다. 새 구현은 request 가 아닌 axios.put 을 사용하므로 transport error 시 콜백 (error, response)response === undefined 경로 자체가 사라졌다. axios 는 transport error 와 non-2xx 응답을 모두 Promise reject 로 표면화하므로 response.statusCode 를 읽다가 TypeError 가 던져질 가능성이 0 이다.

    packages/api/src/api/pano.api.ts (origin/develop)typescript
    uploadImage = async (url: string, filePath: string): Promise\<void> => \{
        await uploadFile(url, filePath, \{
            'Content-Type': 'binary/octet-stream',
        \});
    \};
    
  • 단기 개선 (다른 agent 들에 동일 패턴 점검) — 동일 commit 이 thumbnail.api.ts, forge-api.ts, floorplan-service, pano-postprocessor, pix-genie 등 signed-URL 업로드 경로를 일괄적으로 새 uploadFile 로 라우팅했다 (12 files changed, 345 insertions(+), 197 deletions(-)). singleshot-service.ts:122-127 의 boolean 분기도 제거되어 단순 await 로 바뀌었으며, 실패는 BaseService.checkingQueuetry/catchhandlingMessageErrors(error) 로 흘려보낸다 (packages/base/src/base-service.ts:108-110).

  • 장기 개선 (request → Promise 기반 client 교체, 공통 wrapper)requestaxios.put 으로 교체되었고, packages/api/src/utils/transfer.ts 에 공통 uploadFile + withRetry (RETRYABLE_CODES: ECONNRESET, ETIMEDOUT, ECONNABORTED, EPIPE, EAI_AGAIN; MAX_RETRIES=3; exponential backoff) 가 도입되어 한 곳에서 재시도/로깅을 관리한다. 단, @agents/api@agents/base 를 import 하면 circular workspace dep 가 되므로 동일 retry shape 를 두 패키지가 mirror (packages/api/src/utils/transfer.tspackages/base/src/util/transfer.ts) — 코드 중복이 남아 있다.

추가로 남은 작업#

  • uncaughtException 핸들러 자체는 그대로packages/cupix-tesla-singleshot-agent/src/app.ts:27-37process.on('uncaughtException', …)process.on('unhandledRejection', …) 가 여전히 exitHandler({ exit: true }, 1) 으로 즉시 종료시킨다. 본 cluster 의 path 는 막혔지만, 별개의 라이브러리/콜백에서 동기 throw 가 생기면 같은 패턴으로 task 가 죽을 수 있다 (P2 — 본 cluster 와는 독립).
  • 두 transfer util 의 단일화 — circular dep 회피용 mirror 이지만, retry 상수/코드가 drift 되면 서비스 별 동작이 어긋날 수 있다. 한쪽이 변경되면 다른 쪽도 동기화하는 lint/test 게이트가 있으면 좋다 (없으면 별도 chore).

Monitoring#

  • Datadog: agent 별 Uncaught Exception 발생률 알림. statusCode 키워드가 포함된 uncaught 를 별도 facet 으로 분리.
text
service:cupixworks-capture-singleshot-agent status:error "SYSTEM - Uncaught Exception"
text
service:(cupixworks-capture-singleshot-agent OR cupixworks-capture-postprocessor-agent OR cupixworks-tesla-thumbnail-agent OR cupix-pano-postprocessor) status:error "SYSTEM - Uncaught Exception" "statusCode"
text
service:cupixworks-capture-singleshot-agent status:error ("ETIMEDOUT" OR "image is not uploaded")

(release dashboard 의 timeseries widget 에 그대로 들어갈 수 있는 raw query 만 사용 — | stats, count by(...) 등 monitor-only syntax 미사용.)

Risk Assessment#

  • Risk level: medium — 빈도는 transient (1회 발생) 지만 발생 시 process 강제 종료로 in-flight SQS 메시지의 visibility timeout 이 늘어나고, 같은 패턴이 여러 agent 에 복제되어 있어 다른 서비스에서 재현 가능성 높음.
  • 예상 복잡도: trivialuploadImage 한 함수의 4-5 줄 변경. 호출자 시그니처 변경 없음.
  • Revision 1 기준: origin/develop 에 fix 가 이미 머지되어 있어 재현 위험은 사실상 제거. 잔여 medium 위험은 본 cluster 외의 다른 uncaughtException 경로에 한정.

Revision History#

Revision 1#

Feedback: "origin/develop 에 있는 코드에서 한개로 모았는데 그러면 괜찮나" — origin/develop 에서 업로드 코드를 하나로 통합했는데 그 변경으로 충분한지 확인 요청.

판정:

피드백 항목 판정 근거
origin/develop 의 업로드 통합으로 본 cluster 의 root cause 가 해결되었는가 수용 commit 567dd4802 (TSLA-13233, 2026-06-22, dominik.oh) 에서 applications/agents/packages/api/src/api/pano.api.tsuploadImagerequest callback (if (response.statusCode == 200)) 에서 axios 기반 uploadFile 호출 (await uploadFile(url, filePath, …)) 로 변경됨. axios 는 transport error 와 non-2xx 모두 Promise reject 로 표면화하므로 response === undefined 인 상태로 .statusCode 를 읽는 경로 자체가 존재하지 않음. 호출자 singleshot-service.ts:122-127 의 boolean 분기도 제거되어 await this.cupixApi.pano.uploadImage(…) 단일 라인으로 단순화 (diff: 7 deletions, 2 insertions). 실패 시 throw → BaseService::checkingQueuetry/catch (packages/base/src/base-service.ts:108-110) 가 handlingMessageErrors(error) 로 라우팅하므로 uncaught path 차단됨.
동일 패턴이 다른 agent 들에도 있었는데 모두 모았는가 수용 동일 commit 이 thumbnail.api.ts, forge-api.ts, floorplan-service, pano-postprocessor, pix-genie 의 signed-URL 업로드 경로를 모두 새 uploadFile 로 라우팅 (12 files changed, 345 insertions(+), 197 deletions(-)). RCA "단기 개선" 항목이 충족됨.
공통 wrapper 한 곳에서 관리 — 진짜 "한 곳" 인가 부분 수용 packages/api/src/utils/transfer.tspackages/base/src/util/transfer.ts 두 곳에 retry 로직이 mirror 되어 있다. commit message 와 파일 상단 주석에 따르면 @agents/api@agents/base import 가 circular workspace dep 가 되기 때문에 의도적으로 분리. 따라서 "한 곳" 은 아니지만 두 파일이 동일 shape (RETRYABLE_CODES, MAX_RETRIES=3, exponential backoff) 를 유지하도록 코멘트로 강제됨. 장기적으로는 drift 방지 lint/test 가 별도 필요.
본 cluster 가 fix 됐다면 추가로 남는 위험이 있는가 수용 app.ts:27-37uncaughtException / unhandledRejectionprocess.exit(1) 패턴은 변경되지 않음. 본 cluster 의 path 는 막혔지만 별개 동기 throw 가 생기면 동일 종료 패턴 재현 가능. 본 cluster scope 외 follow-up.

변경 사항:

  • ## Root Cause Summary 상단에 fix 완료 status update note 추가 (commit hash 와 Jira 티켓 포함).
  • ## Fix Recommendation 섹션을 "Status: Already resolved on origin/develop" 헤더로 재구성. 즉시/단기/장기 권장사항을 각각 origin/develop 의 어떤 코드 변경으로 충족됐는지 매핑. 새 uploadImage 코드 스니펫과 commit 567dd4802 의 diff stat 인용.
  • "추가로 남은 작업" 하위 섹션 추가 — uncaughtException 핸들러 자체는 미변경, 두 transfer util 의 drift 위험.
  • ## Risk Assessment 에 Revision 1 기준 재현 위험 사실상 제거 bullet 추가.

추가 조사 내용:

  • $REPOS_DIR/cupixworks 에서 git fetch origin developapplications/agents/packages/api/src/api/pano.api.ts 의 develop HEAD 내용 확인.
  • applications/agents/packages/api/src/utils/transfer.ts (신규 파일) 의 uploadFile, withRetry, RETRYABLE_CODES, MAX_RETRIES 확인.
  • applications/agents/packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts 의 caller 변경 (boolean 분기 제거) 확인.
  • applications/agents/packages/cupix-tesla-singleshot-agent/src/app.tsuncaughtException 핸들러 미변경 확인.
  • applications/agents/packages/base/src/base-service.ts:108-110try { await this.runByMessages(); } catch (error) { await this.handlingMessageErrors(error); } 라우팅 확인.
  • 관련 commit: 567dd4802 TSLA-13233 feat: unify S3/signed-URL uploads onto @agents/base retry infra (HEAD of origin/develop 직전 묶음).