ImageCPObject::downloadFile | path: /tmp/workspace/88774/88774.png, error: {"errno":-104,"code":"ECO
RCA: ImageCPObject::downloadFile ECONNRESET on floorplan 88774
Overview#
What Happened#
cupixworks-any-thumbnail-agent가 SQS 메시지로 받은 Floorplan 88774의 원본 이미지를 다운로드하던 중, 원격지에서 TCP 연결을 끊어 ECONNRESET (errno -104)이 발생했다. 다운로드 시작(16:20:58 KST)에서 에러 발생(16:24:24 KST)까지 약 3분 26초가 경과한 점으로 보아, redirect된 스토리지 응답이 idle 상태에서 끊긴 것이다. 동일 시간대(15분 내) 같은 서비스에서 ETIMEDOUT, "Internal Server Error" 등 비슷한 네트워크/업스트림 장애가 다수 관측된다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | ECONNRESET (Node.js system error) |
| exception.message | read ECONNRESET |
| top_frame | TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20) |
| failure point | packages/cupix-tesla-thumbnail-agent/src/model/common/image-cpobject.ts:82-85 |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| gad / thumbnail-agent (Floorplan) | 1 (이 클러스터) | Floorplan 88774의 thumbnail 생성 실패 — SQS 메시지가 DLQ로 이동했을 가능성 |
| gad / thumbnail-agent (전반) | 동일 시간대 다수 (ETIMEDOUT, 500, "undefined response") |
다른 floorplan/asset 의 thumbnail 생성도 같은 시간대에 함께 실패 |
Timeline#
- 2026-06-19 16:20:58 KST —
BaseService::runByMessage | id: 88774— SQS 메시지 수신, 처리 시작 - 2026-06-19 16:20:58 KST —
ThumbnailService::newModel | model id: 88774, type: Floorplan - 2026-06-19 16:20:58 KST —
ImageCPObject::downloadOriginal | url: http://api-tesla.cupix.internal/api/v1/floorplans/88774/download, path: /tmp/workspace/88774/88774.png - 2026-06-19 16:21:00 KST —
cupixworks-api가[302] GET /api/v1/floorplans/88774/download로 응답 (스토리지로 redirect) - 2026-06-19 16:24:24 KST —
read ECONNRESET(warn) →CupixAuth::handleError | Undefined response(warn) →ImageCPObject::downloadFile | path: /tmp/workspace/88774/88774.png, error: {...ECONNRESET}(error) →BaseService::handlingMessageErrors(error) - 2026-06-19 16:26:01 KST — 같은 서비스에서 다른 floorplan(88747)에 대한
ETIMEDOUT발생 — 일시적 네트워크/업스트림 불안정 패턴 지속 - 2026-06-19 16:26:01 KST — Status board 인시던트
2026-06-19-svc-cupixworks-any-thumbnail-agent-3자동 resolve
Error Log#
ImageCPObject::downloadFile | path: /tmp/workspace/88774/88774.png, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
Impact#
- Service:
cupixworks-any-thumbnail-agent - Team: gad
- 발생 횟수: 1
- 최초 발생: 2026-06-19 16:24:24 KST
- 최근 발생: 2026-06-19 16:24:24 KST
Root Cause Summary#
원본 이미지 다운로드 중 TCP 연결이 피어에 의해 강제 종료되어(ECONNRESET) thumbnail 생성이 실패했다. ImageCPObject.downloadFile은 request.get()을 사용해 원본 URL에서 파일을 스트리밍 다운로드하는데, retry 로직이 statusCode가 있는 HTTP 5xx 에러에만 동작하고 (CupixAuth.retryable이 e.statusCode > 500만 검사), 네트워크 레벨 에러(ECONNRESET, ETIMEDOUT)는 즉시 reject되어 SQS 메시지가 실패 처리된다. 따라서 일시적 네트워크 끊김에 대해 자동 복구가 불가능한 구조이다. 다운로드 URL은 /api/v1/floorplans/88774/download이며 API가 302로 외부 스토리지(presigned URL)로 redirect하므로, 실제 끊긴 연결은 스토리지 측이다.
Technical Analysis#
Code Path#
- Entry point: SQS message handler →
ThumbnailService::newModel | model id: 88774, type: Floorplan - Download orchestration:
ImageCPObject.downloadOriginal(packages/cupix-tesla-thumbnail-agent/src/model/common/image-cpobject.ts:164-181) — 파일이 없으면downloadFile을 호출 - Failure point:
ImageCPObject.downloadFile의request.get(...)'error'핸들러 (image-cpobject.ts:82-85) — 스트리밍 중 ECONNRESET이 발생하면 reject
private downloadFile = (url: string, filePath: string): Promise<void> => new Promise((resolve, reject) => {
const cupixAuth = this.cupixAuth;
cupixAuth.checkToken()
.then(() => {
logger.debug('ImageCPObject::downloadFile | start path: %s', filePath, url);
const fileStream = fs.createWriteStream(filePath);
const sendReq = request.get(url, {
headers: {
'X-CUPIX-AUTH': cupixAuth.accessToken
}
});
// ...
sendReq
.on('response', res => { /* statusCode 검사 */ })
.on('error', err => {
logger.error('ImageCPObject::downloadFile | path: %s, error: %s', filePath, JSON.stringify(err));
reject(cupixAuth.handleError(err));
});
})
.catch((err) => { /* token error */ });
});
downloadFile 자체에는 retry/backoff가 없다. 외부 wrapper(CupixAuth.retryable)에서만 재시도가 일어나는데, 그 조건은 statusCode 기반이다.
retryable = <T>(f: () => Promise<T>, retries?: number): Promise<T> => new Promise((resolve, reject) => {
const _retries = retries != undefined ? retries : 0;
f()
.then(response => resolve(response))
.catch(e => {
if (e && e.statusCode > 500 && _retries < Constants.MaxRetries) {
// ...retry...
}
});
});
기대 동작: 일시적 네트워크 끊김(ECONNRESET/ETIMEDOUT)도 backoff 후 재시도되어야 함.
실제 동작: e.statusCode가 undefined이므로 undefined > 500은 false → 재시도 없음 → SQS 실패.
CupixAuth.handleError는 statusCode가 없을 때 "Undefined response"를 warn으로 남기고 그대로 ec를 반환하므로, 네트워크 에러를 분류해 더 적합한 처리(재시도, 다른 backoff 정책)로 이어지지 않는다.
handleError = (ec: any): any => {
const response = ec && CPUtils.isJsonString(ec) ? JSON.parse(ec) : ec.response;
if (response != undefined) {
// ... statusCode 분기 ...
} else {
logger.warn('CupixAuth::handleError | Undefined response: %s', JSON.stringify(ec, Object.getOwnPropertyNames(ec)));
}
return ec;
};
Log Evidence#
Datadog query (재현용):
service:cupixworks-any-thumbnail-agent "ECONNRESET"
service:cupixworks-any-thumbnail-agent "88774"
같은 trace의 핵심 로그(시간순):
2026-06-19 16:20:58 info BaseService::runByMessage | id: 88774
2026-06-19 16:20:58 info ThumbnailService::newModel | model id: 88774, type: Floorplan
2026-06-19 16:20:58 info ImageCPObject::downloadOriginal | url: http://api-tesla.cupix.internal/api/v1/floorplans/88774/download, path: /tmp/workspace/88774/88774.png
2026-06-19 16:21:00 info [302] GET /api/v1/floorplans/88774/download (Api::V1::FloorplansController#download_single_resource) ← cupixworks-api
2026-06-19 16:24:24 warn read ECONNRESET
2026-06-19 16:24:24 warn CupixAuth::handleError | Undefined response: {"stack":"Error: read ECONNRESET\n at TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20)\n at TLSWrap.callbackTrampoline (node:internal/async_hooks:130:17)","message":"read ECONNRESET","errno":-104,"code":"ECONNRESET","syscall":"read"}
2026-06-19 16:24:24 error ImageCPObject::downloadFile | path: /tmp/workspace/88774/88774.png, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
2026-06-19 16:24:24 error BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-104,"code":"ECONNRESET","syscall":"read"},"sqsMessage":{"MessageId":"ce064437-a2f4-441e-8483-b9cf6f6ac0ca","Attributes":{"ApproximateReceiveCount":"1"}}}
스택 프레임 TLSWrap.onStreamRead은 TLS read 도중(즉, 302 redirect 후 HTTPS 스토리지에서 본문을 받는 중) 끊겼음을 나타낸다. cupixworks-api 측 로그는 302 응답까지만 남고 본문 전송은 외부 스토리지 영역이다.
같은 서비스 동일 시간대의 유사 패턴(클러스터링 근거):
2026-06-19 16:26:01 error ImageCPObject::downloadFile | path: /tmp/workspace/88747/88747.png, error: {"errno":-110,"code":"ETIMEDOUT","syscall":"read"}
2026-06-19 16:22:55 error ImageCPObject::uploadFile | response ... code: 500, message: Internal Server Error
2026-06-19 16:16:08~16:22:55 error ImageCPObject::uploadFile | "undefined response" 다수
Status board 기록:
incident: 2026-06-19-svc-cupixworks-any-thumbnail-agent-3 (resolved)
started_at: 2026-06-19T07:22:55.572Z
resolved_at: 2026-06-19T07:26:01.914Z
clusters: [964edbe6..., 258d1ea0... (this), d1ff47b4...]
직전 24시간 내에 동일 서비스의 같은 incident scope가 4번 자동 발생/해소되었다(...-1, -2, -3, 그리고 06-12 발생). 즉, 단발이 아닌 반복 패턴이다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | cupixworks-api 가 다운로드 응답 자체를 끊었다 |
다운로드 URL이 internal API로 향함 | API는 [302]만 로그 (16:21:00) — 본문 전송은 redirect 대상이 수행. 스택의 TLSWrap.onStreamRead도 TLS 스트림(외부 스토리지)을 가리킴 |
Rejected |
| H2 | 클라이언트 측 token 만료/auth 실패 | cupixAuth.checkToken()이 download 시작 시점에 호출됨 |
에러 객체에 statusCode 없음, 401/403 아님. 메시지가 syscall-level read ECONNRESET |
Rejected |
| H3 | Floorplan 88774가 유효하지 않음(데이터 문제) | — | cupixworks-api 가 302로 정상 redirect — 모델 존재 확인됨 |
Rejected |
| H4 | 외부/스토리지의 일시적 네트워크 끊김 + 클라이언트의 retry 부재로 인한 영구 실패 | 16:20:58 시작 → 16:24:24 끊김(약 3분 26초 idle), read ECONNRESET syscall, 같은 시간대 다른 모델에서도 ETIMEDOUT/undefined response 다발, 24시간 내 동일 service degraded incident 4회 |
— | Confirmed |
| H5 | Sidecar/proxy(예: NLB, NAT, keepalive timeout) idle timeout으로 끊김 | downloadOriginal 시작 후 ~3.5분 후에 정확히 끊김. 일반적으로 AWS NLB idle timeout 350s, NAT 350s 와 근접한 시간대 | 실제 idle 측정값은 로그만으로는 확정 불가 | Inconclusive — needs verification |
Fix Recommendation#
즉시 조치 (Critical)#
-
ImageCPObject.downloadFile에 네트워크 에러 retry 도입: 파일:packages/cupix-tesla-thumbnail-agent/src/model/common/image-cpobject.ts:53-91. 현재는request.get의'error'이벤트가 발생하면 즉시 reject한다. ECONNRESET / ETIMEDOUT / EPIPE / socket hang up 등의 transient 네트워크 에러 코드를 식별하여 exponential backoff로 N회(예: 3회) 재시도한 뒤에만 reject되도록 변경한다. 단, 부분적으로 쓰여진fileStream은 재시도 전에 반드시 cleanup(fileStream.destroy()+fs.unlink)해야 한다. 이미downloadOriginal이 partial file size를 보고 skip하는 로직이 있으므로, 부분 파일 잔존 시 재실행에 영향을 줄 수 있다. -
CupixAuth.retryable의 retry 조건 확장: 파일:packages/agents/api/src/authentication/cupix-auth.ts:59-87. 현재e.statusCode > 500만 retry한다. statusCode가 없는 네트워크 에러(e.code가ECONNRESET/ETIMEDOUT/EPIPE/ENOTFOUND중 하나)도 retry 대상에 포함하도록 조건을 확장한다.
단기 개선 (1주 이내)#
-
ImageCPObject.uploadFile의 path 로깅 버그 수정:image-cpobject.ts:109,114에서path변수가 Node.js의path모듈을 가리키며 객체 dump가 로그에 그대로 찍히고 있다 ({ default: [Object], resolve: [Getter], normalize: [Getter], ... }). 인자명path→filePath로 사용하도록 수정 필요. 이는 RCA·디버깅 시 어떤 모델의 업로드가 실패했는지 식별 불가능하게 만든다. -
SQS DLQ 정책 점검:
BaseService::handlingMessageErrors가 한 번 실패 시 어떻게 동작하는지(DLQ 이동 vs visibility timeout 후 재처리) 확인하고, transient 에러는 메시지 visibility timeout 기반 자연 재시도가 가능하도록 보장한다(ApproximateReceiveCount가 1로 끝나는 점을 보면 재시도가 작동하지 않을 수 있음). -
다운로드 timeout 명시화:
request.get옵션에timeout(예: 60s)을 명시. 현재 default(약 무한 또는 socket-level)는 idle 끊김 감지가 늦다.
장기 개선 (재발 방지)#
-
deprecated
request모듈 교체:request라이브러리는 2020년부터 deprecated 상태.axios/undici/got등 native retry/timeout/keepalive를 지원하는 라이브러리로 점진 교체. -
Network-level retry layer: 모든 agent의 외부 HTTP 호출에 공통 retry/circuit-breaker 미들웨어를 두고, transient vs non-transient 에러를 분류한다.
-
NLB/NAT idle timeout vs HTTP keep-alive 정렬 확인: H5 관련. AWS NLB idle timeout(기본 350s)과 다운로드 stream의 평균 처리 시간이 가까우면 keep-alive 패킷 또는 chunk 흐름으로 idle 발생을 방지하는 옵션을 검토한다.
Monitoring#
writing-datadog-monitoring-queries skill 가이드에 따라 timeseries widget 호환 쿼리만 사용한다 (no | stats, no monitor-only suffix).
- thumbnail-agent의 transient 네트워크 실패 추이(downloadFile/uploadFile 합산):
sum:logs.hits{service:cupixworks-any-thumbnail-agent,status:error,@message:*ECONNRESET*}.as_count()
sum:logs.hits{service:cupixworks-any-thumbnail-agent,status:error,@message:*ETIMEDOUT*}.as_count()
- ImageCPObject 다운로드 실패 빈도:
sum:logs.hits{service:cupixworks-any-thumbnail-agent,status:error,@message:"ImageCPObject::downloadFile"}.as_count()
- 실패 파일별 분포 확인용 (대시보드 group by):
sum:logs.hits{service:cupixworks-any-thumbnail-agent,status:error,@message:"ImageCPObject"} by {message}.as_count()
알림 제안: 5분 간격으로 같은 종류의 transient 실패가 ≥3건이면 Slack 알림. 단발 ECONNRESET 자체는 경고하지 않음.
Risk Assessment#
- Risk level: medium — 단일 클러스터 발생량은 1건이지만 status board에서 24h 내 동일 패턴이 4회 반복되어, 누적 thumbnail 생성 실패가 사용자 경험(Floorplan/Asset 미리보기 누락)에 영향을 줌.
- 예상 복잡도: standard — 잘 알려진 retry 패턴 추가. 다만 partial file cleanup, idempotency, retry budget 설정 등 신중한 구현 필요.