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::uploadStitchedPano → cupixApi.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#
- 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). - 2026-06-19 11:53:12 KST —
BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETIMEDOUT","syscall":"read"}, ...}(직전 SQS 메시지 처리 중 read timeout). - 2026-06-19 11:53:27 KST —
SingleshotService::uploadStitchedPano | image is not uploaded(cluster8803d72d-…와 같은 시점). - 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. - 2026-06-19 11:54:09 KST —
ChildProcessManager::setupEventHandlers | Child process exited/Child process closed가 연쇄적으로 기록됨 (동일/후속 컨테이너의 child process 정리). - 2026-06-19 12:16:02 KST — 후속 메시지 재시도에서 정상 path 의
BaseService::handlingMessageErrors(statusCode 400,RESC10000 Resource does not uploaded) 가 기록됨 (정상 에러 핸들링 — uncaught exception 재현 안 됨).
Error Log#
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로 통합되어requestcallback 의 undefinedresponse경로가 제거됨. 자세한 매핑은 아래 Fix Recommendation 및 Revision History 참고.
packages/api/src/api/pano.api.ts:154-177 의 uploadImage 함수가 request(options, (error, response, body) => …) 콜백에서 error 를 무시하고 response.statusCode 에 직접 접근한다. S3 PUT (또는 그와 동등한 upload URL) 요청이 network error 로 실패하면 request 콜백 규약상 error 만 채워지고 response 는 undefined 가 된다. 이 경우 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:39—new SingleshotService().init() - SQS message loop:
packages/base/src/base-service.ts:86-116(checkingQueue→runByMessages→runByMessage) - Per-capture run:
packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:37-45—run이uploadStitchedPano(targetId)를 await - Upload step:
packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:122—await this.cupixApi.pano.uploadImage(stitchedUploadUrl, stitchedImagePath) - Failure point:
packages/api/src/api/pano.api.ts:170—if (response.statusCode == 200) { ... }(response 가 undefined 일 때 TypeError) - Process-level handler:
packages/cupix-tesla-singleshot-agent/src/app.ts:27-31—process.on('uncaughtException', …)가 메시지를 로깅하고process.exit(1)호출
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 가 채워지고 response 는 undefined 가 될 수 있으므로, error 를 먼저 검사하여 resolve(false) 또는 reject(error) 하고, 그 후 response?.statusCode === 200 처럼 안전하게 접근해야 한다. 실제 동작: error 분기를 무시하고 response.statusCode 를 직접 읽어 TypeError 가 발생한다.
상위 호출자는 이미 false 반환에 대비한 분기를 가지고 있어 정상 실패 시에는 graceful 하게 빠져나간다:
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 종료를 일으킨다:
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 파일에서 그대로 사용):
service:cupixworks-capture-singleshot-agent status:error @environment:production "SYSTEM - Uncaught Exception: Cannot read properties of undefined (reading 'statusCode')"
추가로 사용한 query:
service:cupixworks-capture-singleshot-agent status:error
service:cupixworks-capture-singleshot-agent "statusCode"
핵심 로그 (시간순, KST):
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 를 정상적으로 다룬다.
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 결과:
{
"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 uploaded 와 statusCode TypeError 가 동일 incident 내부의 두 표현임을 확인.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | uploadImage 콜백이 transport error 시 response 가 undefined 인데도 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:358 의 downloadFile 내 res.statusCode 가 원인 |
같은 단어 statusCode 를 다룸 |
해당 함수는 sendReq.on('response') 콜백을 사용하는데, response 이벤트가 발생하면 res 는 정의된 IncomingMessage 객체이다. error 시에는 'error' 이벤트로 가서 reject 되므로 undefined 접근 불가. 또한 runAlignScript 는 await 으로 감싸 정상 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.ts의uploadImage가Promise<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)typescriptuploadImage = 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.checkingQueue의try/catch가handlingMessageErrors(error)로 흘려보낸다 (packages/base/src/base-service.ts:108-110). -
장기 개선 (
request→ Promise 기반 client 교체, 공통 wrapper) —request가axios.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.ts와packages/base/src/util/transfer.ts) — 코드 중복이 남아 있다.
추가로 남은 작업#
uncaughtException핸들러 자체는 그대로 —packages/cupix-tesla-singleshot-agent/src/app.ts:27-37의process.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 으로 분리.
service:cupixworks-capture-singleshot-agent status:error "SYSTEM - Uncaught Exception"
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"
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 에 복제되어 있어 다른 서비스에서 재현 가능성 높음.
- 예상 복잡도: trivial —
uploadImage한 함수의 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.ts 의 uploadImage 가 request 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::checkingQueue 의 try/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.ts 와 packages/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-37 의 uncaughtException / unhandledRejection → process.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코드 스니펫과 commit567dd4802의 diff stat 인용.- "추가로 남은 작업" 하위 섹션 추가 —
uncaughtException핸들러 자체는 미변경, 두 transfer util 의 drift 위험. ## Risk Assessment에 Revision 1 기준 재현 위험 사실상 제거 bullet 추가.
추가 조사 내용:
$REPOS_DIR/cupixworks에서git fetch origin develop후applications/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.ts의uncaughtException핸들러 미변경 확인.applications/agents/packages/base/src/base-service.ts:108-110의try { 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 직전 묶음).