ImageCPObject::putThumbnail | Not json format - <html>
RCA: ImageCPObject::putThumbnail | Not json format - 504 Gateway Time-out
Overview#
What Happened#
2026-05-22 04:29~04:30 UTC에 cupixworks-any-thumbnail-agent 서비스가 ap-southeast-2 리전에서 thumbnail 업로드 시 504 Gateway Time-out 에러를 수신했다. 에이전트는 api-tesla.cupix.internal로 PUT 요청을 보냈으나, nginx reverse proxy가 ~60초 timeout 후 HTML 504 응답을 반환하여 JSON 파싱에 실패했다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | ImageCPObject::putThumbnail |
| exception.message | Not json format - <html>..504 Gateway Time-out...</html> |
| top_frame | image-cpobject.ts:152 |
| env | production, ap-southeast-2 |
Timeline#
- 2026-05-22T04:28:34Z — Asset
6olz8fkpa3v9thumbnail 생성 완료 (1.15초) - 2026-05-22T04:29:35Z — putThumbnail 504 에러 발생 (nginx timeout ~61초 후)
- 2026-05-22T04:30:20Z — Asset
axfq6xp5ew01putThumbnail 504 에러 발생 - 2026-05-22T04:30:20Z — 에이전트는 에러 후 cover 생성 단계로 진행, 작업 완료 후 SQS 메시지 삭제
Error Log#
ImageCPObject::putThumbnail | Not json format - <html>
<head><title>504 Gateway Time-out</title></head>
<body>
<center><h1>504 Gateway Time-out</h1></center>
<hr><center>nginx</center>
</body>
</html>
Impact#
- Service:
cupixworks-any-thumbnail-agent - Team: built
- 발생 횟수: 2 (클러스터 기준; 동일 시간대 총 4건 확인)
- 최초 발생: 2026-05-22T04:29:35.631Z
- 최근 발생: 2026-05-22T04:30:20.271Z
- 영향: thumbnail 업로드 실패 시
resolve(undefined)처리되어 에이전트가 정상 완료로 간주. SQS 메시지가 삭제되므로 실패한 thumbnail 업로드는 재시도되지 않음. 사용자 측에서는 thumbnail이 누락된 상태로 남음.
Root Cause Summary#
putThumbnail 메서드가 api-tesla.cupix.internal에 multipart PUT 요청으로 thumbnail을 업로드할 때, upstream Rails API가 ~60초 내에 응답하지 못하여 nginx가 504 Gateway Time-out HTML을 반환했다. 에이전트 코드는 응답 body가 JSON인지 확인(CPUtils.isJsonString)하여, JSON이 아닌 경우 에러를 로깅하되 resolve(undefined)로 처리한다. 이로 인해 상위 호출자(updateThumbnail)는 정상 완료로 간주하고, 재시도 없이 다음 단계(cover 생성)로 진행한다.
upstream API가 느린 원인은 해당 시간대 ap-southeast-2 리전의 API 서버 부하 또는 일시적 네트워크 지연으로 추정된다. cupixworks-api 로그에서 해당 시간대 504/timeout 에러는 확인되지 않았으므로, 요청이 API 서버에 도달하기 전 또는 처리 중 nginx proxy timeout에 걸린 것으로 판단된다.
Technical Analysis#
Code Path#
- Entry point:
thumbnail-service.ts:80—await supportModel.updateThumbnail() updateThumbnail호출:image-cpobject.ts:214-226—putThumbnail내부 호출- Failure point:
image-cpobject.ts:146-153— 응답 body가 JSON이 아닌 경우 에러 로깅 후resolve(undefined)
private putThumbnail = (url: string, filePath: string): Promise<T | undefined> => new Promise((resolve, reject) => {
const cupixAuth = this.cupixAuth;
cupixAuth.checkToken()
.then((_) => {
// ...options setup...
request.put(url, options, (error, response, body) => {
if (error) {
reject(cupixAuth.handleError(error));
} else {
if (CPUtils.isJsonString(body)) {
const res = JSON.parse(body);
const model = res?.result?.data ? res.result.data.attributes : undefined;
resolve(model);
} else {
logger.error('ImageCPObject::putThumbnail | Not json format - %s', body);
resolve(undefined); // ← 에러 상황인데 resolve로 처리
}
}
});
})
.catch((err) => {
reject(err);
});
});
updateThumbnail 메서드는 putThumbnail의 반환값을 사용하지 않고, reject되지 않는 한 정상 흐름으로 진행한다:
updateThumbnail = async (): Promise<void> => {
logger.debug('ImageCPObject::updateThumbnail | begin');
if (Environment.DEBUG_MODE) {
logger.info('ImageCPObject::updateThumbnail | skipped - DEBUG_MODE');
return;
}
if (this._updateThumbnailServerUrl == undefined || this._thumbnailFilePath == undefined) {
logger.warn('ImageCPObject::updateThumbnail | end - undefined thumbnail server url or file path');
return;
}
await this.putThumbnail(this._updateThumbnailServerUrl, this._thumbnailFilePath);
logger.debug('ImageCPObject::updateThumbnail | end');
};
thumbnail-service.ts:76-81에서 updateThumbnail 이후 cover 생성으로 이어지며, 에러가 throw되지 않으므로 SQS 메시지가 삭제된다:
if (isContinue) {
await supportModel.downloadOriginal();
await supportModel.preprocess();
await supportModel.generateThumbnail();
await supportModel.updateThumbnail(); // ← 504 시에도 정상 완료
}
if (supportModel.needCover) {
await supportModel.downloadOriginal();
await supportModel.preprocess();
await supportModel.generateCover();
await supportModel.uploadCover();
}
Log Evidence#
Datadog 검색 쿼리:
service:cupixworks-any-thumbnail-agent status:error @environment:production "ImageCPObject::putThumbnail"
에러 로그 (2건, 동일 패턴):
2026-05-22T04:29:35.631Z | host:ip-10-1-40-198 | asset.key:6olz8fkpa3v9
ImageCPObject::putThumbnail | Not json format - <html>\r\n<head><title>504 Gateway Time-out</title></head>...
2026-05-22T04:30:20.271Z | host:ip-10-1-162-22 | asset.key:axfq6xp5ew01
ImageCPObject::putThumbnail | Not json format - <html>\r\n<head><title>504 Gateway Time-out</title></head>...
타이밍 분석 (asset 6olz8fkpa3v9):
04:28:34Z — generateThumbnail 완료 (1.15s)
04:29:35Z — putThumbnail 에러 발생 (generateThumbnail 완료 후 ~61초 경과 = nginx timeout)
동일 시간대 cupixworks-api 에러 검색:
service:cupixworks-api status:error @environment:production
결과: 504/timeout 관련 에러 없음. Elasticsearch mapper_parsing_exception 에러만 확인됨 (무관).
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Upstream API 서버가 ~60초 내 응답 실패 → nginx 504 반환 | 로그 타이밍 분석: generateThumbnail 완료 후 61초 뒤 504 수신 (nginx timeout 기본값 60초와 일치). 여러 호스트에서 동시 발생. | — | Confirmed |
| H2 | API 서버 자체 에러/크래시로 요청 실패 | 다른 호스트에서도 동시 발생 | cupixworks-api 로그에 해당 시간대 504/timeout 에러 없음. API 서버가 요청 처리 실패를 기록하지 않은 점은 요청이 도달했으나 처리 지연된 것을 시사 | Rejected |
| H3 | 네트워크 장애로 API 서버 연결 불가 | ap-southeast-2 리전 한정 발생 | 연결 불가 시 ECONNREFUSED 등 connection error가 발생해야 하나, HTTP 504 응답을 정상 수신함. nginx가 응답한 것이므로 네트워크 자체는 정상 |
Rejected |
| H4 | 대용량 파일 업로드로 인한 timeout | multipart PUT으로 thumbnail 전송 | thumbnail은 일반적으로 소용량 (수십KB), 1.15초에 생성된 점으로 볼 때 원본도 소형 이미지. 업로드 자체가 아니라 API 처리 시간이 문제 | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
image-cpobject.ts:152— 504/5xx 응답 시resolve(undefined)대신reject(new Error(...))로 변경하여 상위 호출자에서 에러를 인지할 수 있도록 하거나, 최소한 retry 로직을 추가해야 한다.putThumbnail메서드에 HTTP timeout 설정 추가 (request.putoptions에timeout파라미터). 현재 timeout 미설정으로 nginx의 60초 timeout에 의존하고 있다.
단기 개선 (1주 이내)#
putThumbnail실패 시 exponential backoff retry (최대 2-3회) 구현. 일시적 API 지연에 대한 복원력 확보.- HTTP 응답 status code를 먼저 확인하고, 5xx 에러 시 body 파싱을 시도하지 않도록 response 객체 활용. 현재는
request라이브러리의 callback에서response객체를 활용하지 않고body만 확인하고 있다.
장기 개선 (재발 방지)#
request라이브러리는 deprecated 상태.axios또는node-fetch로 마이그레이션하면 timeout, retry, 응답 상태 관리가 용이해진다.- thumbnail 업로드 실패 시 SQS 메시지를 삭제하지 않고 retry queue로 이동하거나, Dead Letter Queue(DLQ) 활용을 검토.
Monitoring#
- Thumbnail 업로드 실패율 추적:
service:cupixworks-any-thumbnail-agent status:error "putThumbnail"
- API 응답 시간 모니터링 (p95, p99):
service:cupixworks-api @http.url:*/assets/*/thumbnail @http.status_code:504
- 알림 조건: 동일 리전에서 5분 내 putThumbnail 에러 3건 이상 발생 시
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard
- 발생 빈도가 낮고 (2건), 사용자 영향은 thumbnail 미노출에 국한된다. 다만 SQS 메시지가 삭제되어 자동 복구 기회가 없으므로, thumbnail이 영구 누락될 수 있다.