ES /docs

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#

  1. 2026-05-22T04:28:34Z — Asset 6olz8fkpa3v9 thumbnail 생성 완료 (1.15초)
  2. 2026-05-22T04:29:35Z — putThumbnail 504 에러 발생 (nginx timeout ~61초 후)
  3. 2026-05-22T04:30:20Z — Asset axfq6xp5ew01 putThumbnail 504 에러 발생
  4. 2026-05-22T04:30:20Z — 에이전트는 에러 후 cover 생성 단계로 진행, 작업 완료 후 SQS 메시지 삭제

Error Log#

Datadog Logs

text
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:80await supportModel.updateThumbnail()
  • updateThumbnail 호출: image-cpobject.ts:214-226putThumbnail 내부 호출
  • Failure point: image-cpobject.ts:146-153 — 응답 body가 JSON이 아닌 경우 에러 로깅 후 resolve(undefined)
image-cpobject.ts:120-162typescript
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되지 않는 한 정상 흐름으로 진행한다:

image-cpobject.ts:214-226typescript
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 메시지가 삭제된다:

thumbnail-service.ts:76-88typescript
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 검색 쿼리:

text
service:cupixworks-any-thumbnail-agent status:error @environment:production "ImageCPObject::putThumbnail"

에러 로그 (2건, 동일 패턴):

text
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):

text
04:28:34Z — generateThumbnail 완료 (1.15s)
04:29:35Z — putThumbnail 에러 발생 (generateThumbnail 완료 후 ~61초 경과 = nginx timeout)

동일 시간대 cupixworks-api 에러 검색:

text
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.put options에 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 업로드 실패율 추적:
text
service:cupixworks-any-thumbnail-agent status:error "putThumbnail"
  • API 응답 시간 모니터링 (p95, p99):
text
service:cupixworks-api @http.url:*/assets/*/thumbnail @http.status_code:504
  • 알림 조건: 동일 리전에서 5분 내 putThumbnail 에러 3건 이상 발생 시

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 발생 빈도가 낮고 (2건), 사용자 영향은 thumbnail 미노출에 국한된다. 다만 SQS 메시지가 삭제되어 자동 복구 기회가 없으므로, thumbnail이 영구 누락될 수 있다.