BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-104,"code":"ECONN
RCA: FloorplanService::downloadFile ECONNRESET while fetching original floorplan
Overview#
What Happened#
cupixworks-any-floorplan-agent(production, us-west-2)가 SQS 메시지 86b02f14-377e-4fa7-9710-24f2de51103e(floorplan id 89294) 를 처리하는 중 원본 PNG 파일을 다운로드하다가 TLS 소켓이 상대방에 의해 끊겨 ECONNRESET (errno -104, syscall: read) 으로 실패했다. 단발성 네트워크 끊김이며, 동일 메시지/같은 floorplan은 후속 redrive 와 다른 메시지에서 정상 처리되었다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Error (Node.js system error) |
| exception.code | ECONNRESET |
| exception.errno | -104 |
| exception.syscall | read |
| top_frame | node:internal/stream_base_commons:218:20 (TLSWrap.onStreamRead) |
| failure point | packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:267-270 |
| sqs.message_id | 86b02f14-377e-4fa7-9710-24f2de51103e |
| sqs.receive_count | 4 (재시도 중) |
| floorplan.id | 89294 |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| chrischoi / floorplan-agent | 1 메시지 (errno=-104 발생 2건이 14일 창에 산발) | 단일 floorplan(89294) 처리 한 번 실패 — 같은 메시지가 SQS visibility timeout 후 재전달되어 후속 처리에서 성공. 사용자 가시 영향 없음. |
Timeline#
- 2026-06-25 00:01:13 KST —
FloorplanService::downloadFile가/tmp/workspace/89294/89294.png다운로드 중ECONNRESET발생 (request.get(...).on('error', ...)콜백에서cupixAuth.handleError호출 → reject) - 2026-06-25 00:01:13 KST —
BaseService::handlingMessageErrors가 errno/code/syscall 을 가진 시스템 에러로 인식, 메시지를 SQS 에서 삭제하지 않고 종료 - 2026-06-25 02:55 KST 무렵 (이후) — 같은 floorplan id
89294가 다른 메시지로 재처리되어 정상 완료 (tileFloorplan | tiling done로그 확인) - 2026-06-25 동일 일자 17:55, 19:56, 21:57 KST —
id: 89294추가 처리 모두 성공 (재발 없음)
Error Log#
BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-104,"code":"ECONNRESET","syscall":"read"},"sqsMessage":{"MessageId":"86b02f14-377e-4fa7-9710-24f2de51103e","Attributes":{"ApproximateReceiveCount":"4"}}}
Impact#
- Service:
cupixworks-any-floorplan-agent - Team: chrischoi
- 발생 횟수: 1
- 최초 발생: 2026-06-25 00:01:13 KST
- 최근 발생: 2026-06-25 00:01:13 KST
Root Cause Summary#
FloorplanService.downloadFileWithHeader 가 원본 floorplan PNG 를 HTTPS GET 으로 받는 도중 상대 측(스토리지/CDN) 이 TLS 연결을 끊어 Node.js 가 ECONNRESET (errno=-104, syscall=read) 시스템 에러를 throw 했다. 이는 코드 결함이 아니라 외부 의존성(원격 호스트 또는 그 사이의 네트워크 장비)의 단발성 연결 종료다. 코드 측 안전망은 이미 의도대로 동작했다 — BaseService.getApiErrorToDeleteMessage 가 errno/code/syscall 시그니처로 이를 "nodejs common system error" 로 분류해 undefined 를 반환했고, 그 결과 SQS 메시지를 삭제하지 않아 visibility timeout 후 재전달되었으며 후속 처리에서 성공했다. 다만 이 경로에는 애플리케이션 레벨 재시도가 없어, ECONNRESET 한 번에 한 개 메시지가 한 번 실패 처리되고 SQS 재전달에 의존한다.
Technical Analysis#
Code Path#
- Entry point:
packages/base/src/base-service.ts:107-110—runByMessages가 던진 에러를handlingMessageErrors로 위임 - Download issue:
packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:76-80—downloadFileWithHeader(downloadUrl, originalFilePath, headers)호출 - Failure point:
packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:267-270—request.get(...).on('error', ...)콜백이JSON.stringify(err)로 로그 후cupixAuth.handleError(err)결과를 reject - System-error short-circuit:
packages/base/src/base-service.ts:245-248—errno/code/syscall트리오면 즉시 return → 메시지 삭제 안 함 → SQS 재전달 의존
private downloadFileWithHeader = (url: string, path: string, headers?: any): Promise<void> => new Promise((resolve, reject) => {
this.setErrorCode(ErrorCode.Agent.FailedToDownloadOriginalFiles);
const cupixAuth = this.cupixAuth;
cupixAuth.checkToken()
.then(() => {
logger.debug('FloorplanService::downloadFile | start path: %s, url: %s', path, url);
const fileStream = fs.createWriteStream(path);
const sendReq = request.get(url, {
headers: headers != undefined ? headers : {
'X-CUPIX-AUTH': cupixAuth.accessToken
}
});
fileStream
.on('finish', async () => { /* ...resolve */ });
sendReq
.on('response', res => {
if (res.statusCode !== 200) { reject(cupixAuth.handleError(res)); }
else { sendReq.pipe(fileStream); }
})
.on('error', err => {
logger.error('FloorplanService::downloadFile | path: %s, error: %s', path, JSON.stringify(err));
reject(cupixAuth.handleError(err));
});
})
private getApiErrorToDeleteMessage = (error: any): any => {
if (error == undefined) { /* ... */ }
if (error.errno != undefined && error.code != undefined && error.syscall != undefined) {
logger.warn('BaseService::getApiErrorToDeleteMessage | nodejs common system error', error);
return; // ECONNRESET → return undefined → message NOT deleted
}
// ...statusCode 4xx/5xx 일 때만 errorMsg 반환
};
try {
await this.runByMessages();
} catch (error) {
await this.handlingMessageErrors(error); // 에러 1회 로그 후 메시지는 삭제하지 않음
}
this.resetMessages();
기대 동작: 일시적 네트워크 끊김 → SQS visibility timeout 후 재전달 → 다른 처리에서 성공.
실제 동작: 동일하게 진행됨. 사용자 가시 영향 없음 (id: 89294 가 같은 날 4회 이상 정상 처리됨이 info 로그로 확인됨).
Log Evidence#
Datadog query (재현용):
service:cupixworks-any-floorplan-agent ECONNRESET
핵심 로그 (시간순, KST):
2026-06-25 00:01:13 warn read ECONNRESET
2026-06-25 00:01:13 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-25 00:01:13 error FloorplanService::downloadFile | path: /tmp/workspace/89294/89294.png, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
2026-06-25 00:01:13 error BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-104,"code":"ECONNRESET","syscall":"read"},"sqsMessage":{"MessageId":"86b02f14-377e-4fa7-9710-24f2de51103e","Attributes":{"ApproximateReceiveCount":"4"}}}
같은 floorplan id 89294 가 같은 날 후속 처리에서 정상 완료된 증거 (Datadog query: service:cupixworks-any-floorplan-agent "89294"):
2026-06-25 02:00:34 info FloorplanService::tileFloorplan | tiling done and now save to `/tmp/workspace/89294/tile`
2026-06-25 01:59:28 info BaseService::runByMessage | id: 89294
2026-06-24 21:58:29 info FloorplanService::tileFloorplan | tiling done and now save to `/tmp/workspace/89294/tile`
2026-06-24 19:57:40 info FloorplanService::tileFloorplan | tiling done and now save to `/tmp/workspace/89294/tile`
2026-06-24 17:56:35 info FloorplanService::tileFloorplan | tiling done and now save to `/tmp/workspace/89294/tile`
빈도(14일 retention 창, service:cupixworks-any-floorplan-agent ECONNRESET status:error):
2026-06-25 00:01:13 (89294)
2026-06-23 11:19:09 (89075)
→ 14일 동안 2회의 단발성 발생. 시스템적인 패턴 아님.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 원격 스토리지/CDN 또는 중간 네트워크 장비가 idle/유휴 TLS 소켓을 RST 로 종료 (transient) | errno=-104, code=ECONNRESET, syscall=read; stack TLSWrap.onStreamRead; CupixAuth::handleError 의 Undefined response (HTTP 응답 없는 read 시점 에러); 14일 창에 2회만 산발; 같은 id:89294 가 같은 날 4회 이상 성공 |
— | Confirmed |
| H2 | Floorplan 89294 자체의 영구적 데이터 문제 | 메시지 ApproximateReceiveCount=4 | 같은 id 가 동일 일자에 17:56, 19:57, 21:58 KST, 다음날 02:00 KST 까지 tileFloorplan tiling done 로그로 정상 처리 확인 |
Rejected |
| H3 | 인증 토큰 만료/회전이 read 중 끊김을 유발 | CupixAuth::handleError 가 로그에 등장 |
handleError 는 단순 패스스루 로깅(file cupix-auth.ts:42-57)이며 토큰 갱신 호출 없음. 또한 checkToken 은 이미 downloadFileWithHeader 시작 시점에 통과한 상태에서 실패. errno/syscall 시그니처는 인증 실패가 아닌 소켓 read 단계의 OS 레벨 에러. |
Rejected |
| H4 | 디스크 가득 또는 fs.createWriteStream 실패 | — | 에러는 sendReq.on('error') 의 read ECONNRESET 으로, fileStream 'error' 가 아님. fileStream finish 도 호출되지 않은 상태. |
Rejected |
| H5 | 외부 의존성/리전 단위 광범위 장애 | — | status-board: svc:cupixworks-any-floorplan-agent::unknown 의 active=null, recent=[]. 다른 서비스에서 동시각 ECONNRESET 폭증 증거 없음. |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 없음. 단발성 외부 네트워크 장애이며 SQS redrive 로 자체 복구된다. 코드 변경 불필요.
- 다만 모니터링 임계치(아래 Monitoring 섹션) 만 점검할 것.
단기 개선 (1주 이내)#
- 로그 레벨 조정 검토:
packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:268의logger.error와packages/base/src/base-service.ts:311의 최종logger.error는 시스템 errno 기반 transient 케이스에서 함께 트리거되어 RCA 노이즈를 발생시킨다.getApiErrorToDeleteMessage가 "nodejs common system error" 로 분류한 경우(errno/code/syscall트리오) 는handlingMessageErrors의 최종 라인을logger.warn으로 다운그레이드 검토. 메모리 노트의 "Cross-region tokens, rate limits, and transient network issues may warrantwarn-level logging" 패턴과 일치. - 다운로드 단계 in-process 재시도:
downloadFileWithHeader에cupixAuth.retryable와 동일한 패턴(지수 백오프, 최대 N회) 의 ECONNRESET/ETIMEDOUT 한정 재시도를 추가 검토. 현재는 SQS visibility timeout(10분,QueueVisibilityTimeout = 600) 을 기다린 뒤에야 재처리되므로 사용자 체감 latency 가 길어진다.
장기 개선 (재발 방지)#
- HTTPS keep-alive idle timeout 튜닝: 원본 다운로드에 사용하는
request인스턴스의 keep-alive idle 시간을 원격 호스트(스토리지/CDN) 의 idle close 보다 짧게 설정해 RST 가 발생하는 idle window 자체를 회피. request(deprecated) →axios/undici/got등으로의 단계적 마이그레이션. 현재 코드 베이스에는 이미axios관습이 있으므로(cupix-auth.ts응답 호환 분기) 통일 검토.
Monitoring#
- 추가/유지할 메트릭/알림:
- 다운로드 ECONNRESET 시계열(이상 급증 감지)
- 서비스 status:error 트렌드
- Datadog timeseries 쿼리 (release dashboard widget 호환 —
writing-datadog-monitoring-queries가이드 적용):
sum:datadog.estimated_usage.logs.ingested_events{service:cupixworks-any-floorplan-agent,status:error}.as_count()
logs("service:cupixworks-any-floorplan-agent ECONNRESET").index("*").rollup("count").by("status")
logs("service:cupixworks-any-floorplan-agent FloorplanService::downloadFile error").index("*").rollup("count")
알림 임계: 시간당 ECONNRESET 5회 초과 또는 24시간 누적 20회 초과 시 page (현재 14일 2회 대비 충분한 헤드룸).
Risk Assessment#
- Risk level: low — 단발성 외부 네트워크, 자체 복구 메커니즘(SQS redrive) 정상 동작, 사용자 가시 영향 없음 확인.
- 예상 복잡도: trivial (코드 변경 시) — 로그 레벨 다운그레이드 1라인 또는 다운로드 재시도 래퍼.