ES /docs

PotreeService::handlingMessageErrors | Error and message object - {"error":"undefined response","sqs

RCA: PotreeService::handlingMessageErrors | undefined response

Overview#

What Happened#

2026-04-24 19:41 UTC에 cupixworks-any-potree-r1-xlarge-agent 서비스에서 pointcloud 1054099에 대한 potree 변환 작업 중 "undefined response" 에러가 1건 발생했다. 동일 pointcloud가 짧은 시간 내 두 번 처리되면서, 두 번째 시도가 이미 uploaded 상태인 potree에 대해 upload credentials를 요청하여 API가 HTTP 400을 반환했고, 에러 핸들링 코드가 원본 API 응답 정보를 소실하여 "undefined response"로 기록되었다.

Quick Facts#

Field Value
exception.message undefined response
top_frame potree-service.ts:456-468 (getApiErrorToDeleteMessage)
env production, us-west-2

Timeline#

  1. 19:30:22 UTC — Agent가 SQS 메시지를 수신하여 pointcloud 1054099 처리 시작 (potree_state: created)
  2. 19:36:18 UTC — 동일 pointcloud에 대한 두 번째 처리 시도 시작 (potree_state: uploading)
  3. 19:37-19:39 UTC — PotreeConverter 실행 (두 시도 모두)
  4. 19:40:34.195 UTC — 두 번째 시도에서 potree_upload_credentials API 호출 시 HTTP 400 반환 (InvalidState: potree_state is uploaded state)
  5. 19:40:34.196 UTCgetApiErrorToDeleteMessage에서 "undefined response" warn 로그
  6. 19:40:34.230 UTC — SQS 메시지 정상 삭제
  7. 19:41:04.540 UTChandlingMessageErrors에서 최종 error 로그 기록
  8. 19:56:45 UTC — workspace 정리 완료

Error Log#

Datadog Logs

text
PotreeService::handlingMessageErrors | Error and message object - {"error":"undefined response","sqsMessage":{"MessageId":"50901316-96a7-478a-9b3c-4e0d89b8b5cc","Attributes":{"ApproximateReceiveCount":"1"}}}

Impact#

  • Service: cupixworks-any-potree-r1-xlarge-agent
  • 발생 횟수: 1
  • 최초 발생: 2026-04-24T19:41:04.540Z
  • 최근 발생: 2026-04-24T19:41:04.540Z

SQS 메시지는 정상적으로 삭제되었고, pointcloud 1054099의 potree_state는 첫 번째 시도에서 이미 uploaded로 전환 완료되었으므로 사용자 영향은 없다. 다만 에러 핸들러가 potree_stateerror로 업데이트하려 시도했을 가능성이 있어, 이미 uploaded된 상태를 덮어쓸 위험이 존재한다.

Root Cause Summary#

uploadPotreeFiles 메서드(potree-service.ts:346-348)에서 createPotreeUploadCredentials API 호출이 실패할 때, 원본 API 에러 객체를 new Error(...) 문자열로 감싸서 재throw한다. 이로 인해 원본 에러의 .response 프로퍼티가 소실된다. 이후 getApiErrorToDeleteMessage(potree-service.ts:456-490)가 에러에서 .response를 추출하려 하지만 undefined이므로 "undefined response"를 반환한다. 근본적으로는 동일 pointcloud(1054099)가 짧은 시간 내 두 번 처리된 것이 trigger이며, 두 번째 시도 시점에 첫 번째 시도가 이미 potree 업로드를 완료하여 API가 InvalidState (HTTP 400)를 반환한 것이다.

Technical Analysis#

Code Path#

  • Entry point: potree-service.ts:54 (checkingQueue)
  • checkingQueue에서 runByMessages()를 호출하고, 에러 발생 시 handlingMessageErrors로 전달 (potree-service.ts:74-78)
potree-service.ts:74-78typescript
try {
    await this.runByMessages();
} catch (error) {
    await this.handlingMessageErrors(error);
}
  • runByMessage (potree-service.ts:180-226)에서 pointcloud의 potreeStateUploaded가 아닌 경우 변환 + 업로드 파이프라인 실행
potree-service.ts:200-214typescript
if (cpPointcloud.potreeState !== TESLA.PointcloudPotreeState.Uploaded) {
    await this.updatePotreeState(targetId, TESLA.PointcloudPotreeState.Processing);
    if (cpPointcloud.downloadUrl != undefined && cpPointcloud.originalFilePath != undefined) {
        await this.downloadFile(cpPointcloud.downloadUrl, cpPointcloud.originalFilePath);
        const entityParameters = await this.loadEntityParameter(cpPointcloud.id);
        const options = await this.createOptions(entityParameters);
        await this.runPotreeConvertor(cpPointcloud, options);
        await this.checkPotreeEntryFile(cpPointcloud);
        await this.uploadPotreeFiles(cpPointcloud);
        // ...
    }
}
  • Failure point: uploadPotreeFiles (potree-service.ts:344-348)에서 createPotreeUploadCredentials 호출 실패 시 원본 에러를 new Error(...) 문자열로 감싸서 throw
potree-service.ts:344-348typescript
try {
    s3Credentials = await this.cupixApi.pointcloud.createPotreeUploadCredentials(cpPointcloud.id);
} catch (error) {
    throw new Error(`PotreeService::uploadPotreeFiles - ${error}`);
}

new Error(...) 래핑이 원본 에러 객체의 .response 프로퍼티를 제거한다. 원본 에러에는 response.statusCode: 400, response.body.result 등 API 응답 정보가 포함되어 있었지만, 문자열 변환 과정에서 모두 소실된다.

  • handlingMessageErrors (potree-service.ts:502-525)에서 getApiErrorToDeleteMessage를 호출
potree-service.ts:502-523typescript
private handlingMessageErrors = async (error: any): Promise<void> => {
    const errorAndMessage = {
        error: error,
        sqsMessage: {}
    };
    if (this.messageInProcess) {
        errorAndMessage.sqsMessage = {
            MessageId: this.messageInProcess.MessageId,
            Attributes: this.messageInProcess.Attributes
        };
        const apiErrorObject = this.getApiErrorToDeleteMessage(error);
        if (apiErrorObject != undefined || this.checkReceiveCountToDeleteMessage()) {
            try {
                errorAndMessage.error = apiErrorObject;
                await this.deleteByMessage(this.messageInProcess);
                if (this._modelInProcess != undefined) await this.updatePotreeState(this._modelInProcess.id, TESLA.PointcloudPotreeState.Error);
            } catch (error) {
                logger.error('PotreeService::handlingMessageErrors | Errors in error handling - %s', JSON.stringify(error));
            }
        }
    }
    logger.error('PotreeService::handlingMessageErrors | Error and message object - %s', JSON.stringify(errorAndMessage));
};
  • getApiErrorToDeleteMessage (potree-service.ts:456-490)에서 error.responseundefined이므로 'undefined response' 반환
potree-service.ts:465-468typescript
const response = CPUtils.isJsonString(error) ? JSON.parse(error) : error.response;
if (response == undefined) {
    logger.warn('PotreeService::getApiErrorToDeleteMessage | undefined response - %s', JSON.stringify(error));
    return 'undefined response';
}

errornew Error("PotreeService::uploadPotreeFiles - [object Object]") 형태의 Error 객체이므로:

  • CPUtils.isJsonString(error)false (Error 객체는 JSON 문자열이 아님)
  • error.responseundefined (표준 Error 객체에는 .response가 없음)
  • 결과: 'undefined response' 반환

추가 문제: handlingMessageErrors에서 apiErrorObject'undefined response' (truthy)이므로 updatePotreeState(id, Error)를 호출하여 이미 uploaded 상태인 pointcloud의 potree_stateerror로 변경할 수 있다 (line 517).

Log Evidence#

Datadog 검색 쿼리:

text
service:cupixworks-any-potree-r1-xlarge-agent status:* @environment:production

핵심 로그 시퀀스 (pointcloud 1054099):

text
19:30:22.612 [info]  PotreeService::runByMessage | id: 1054099
19:30:27.005 [info]  PotreeService::runByMessage | state: queued, resource_state: uploaded, potree_state: created
19:36:18.980 [info]  PotreeService::runByMessage | id: 1054099
19:36:27.602 [info]  PotreeService::runByMessage | state: queued, resource_state: uploaded, potree_state: uploading

두 번째 시도 시점에 potree_state: uploading인 것은 첫 번째 시도가 아직 진행 중이었음을 의미한다. 두 번째 시도도 Uploaded가 아니므로 변환 파이프라인을 다시 시작했다.

API 에러 로그:

text
19:40:34.195 [warn]  CupixAuth::handleError | Response statusCode: 400, requestUriHref: http://api-tesla.cupix.internal/api/v1/pointclouds/1054099/potree_upload_credentials?fields[...], body.result: {"code":"STAT10000","type":"Cupix::Errors::InvalidState","reason":"potree_state is uploaded state","message":"potree_state is uploaded state"}
text
19:40:34.196 [warn]  PotreeService::getApiErrorToDeleteMessage | undefined response - {}

JSON.stringify(error) 결과가 {}인 것은 Error 객체의 standard properties(message, stack)가 non-enumerable이기 때문이다. 이는 new Error(...) 래핑의 직접적 증거이다.

text
19:41:04.540 [error] PotreeService::handlingMessageErrors | Error and message object - {"error":"undefined response","sqsMessage":{"MessageId":"50901316-96a7-478a-9b3c-4e0d89b8b5cc","Attributes":{"ApproximateReceiveCount":"1"}}}

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 uploadPotreeFiles에서 new Error(...) 래핑으로 원본 API 에러의 .response 프로퍼티가 소실되어 getApiErrorToDeleteMessage가 응답을 파싱하지 못함 warn 로그에서 JSON.stringify(error) = {}로 Error 객체임을 확인 (potree-service.ts:467). 코드에서 throw new Error(...) 확인 (potree-service.ts:347). getApiErrorToDeleteMessage.response 접근 시 undefined (potree-service.ts:465) Confirmed
H2 API가 비정상적인 응답 형식을 반환하여 파싱 실패 CupixAuth::handleError 로그에서 API 응답이 정상 JSON 형식 ({"code":"STAT10000",...})으로 확인됨. 문제는 응답 형식이 아니라 에러 래핑에서 .response가 사라진 것 Rejected
H3 SQS 메시지 중복 수신 없이 단독 처리 중 에러 발생 로그에서 동일 pointcloud 1054099가 19:30과 19:36에 두 번 처리된 것을 확인. 두 번째 시도 시 potree_state: uploading (첫 번째 시도 진행 중). API가 InvalidState: potree_state is uploaded state로 응답하여 경쟁 조건 확인 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • potree-service.ts:346-348: throw new Error(...) 대신 원본 에러 객체를 그대로 throw하도록 변경. 이렇게 하면 getApiErrorToDeleteMessage.response 프로퍼티에 접근하여 API 에러 정보를 정상적으로 추출할 수 있다.

단기 개선 (1주 이내)#

  • potree-service.ts:200: potreeState 체크 로직 강화. 현재 Uploaded만 skip하고 uploading, processing 등 중간 상태에서도 변환을 시작한다. uploading 또는 processing 상태일 때도 skip하거나 대기하는 로직 추가.
  • potree-service.ts:517: handlingMessageErrors에서 updatePotreeState(Error) 호출 전에 현재 potreeState를 확인하여, 이미 uploaded인 경우 error로 덮어쓰지 않도록 방어 로직 추가.

장기 개선 (재발 방지)#

  • SQS visibility timeout 또는 deduplication 설정 검토. 동일 pointcloud가 짧은 시간 내 두 번 처리되는 경쟁 조건을 방지하기 위해 FIFO 큐 또는 idempotency 메커니즘 도입 검토.
  • potree agent가 BaseService를 상속하지 않고 독립적인 PotreeService 클래스를 사용하고 있어, BaseService의 개선사항(tracing, 로깅 등)이 반영되지 않는다. BaseService 상속 구조로 마이그레이션하면 에러 핸들링 일관성이 향상된다.

Monitoring#

  • potree agent에서 "undefined response" 패턴의 에러를 모니터링하여 에러 래핑 수정 후 재발 여부 확인:
text
service:cupixworks-any-potree-*-agent "undefined response" status:error
  • InvalidState API 에러 빈도를 모니터링하여 경쟁 조건의 규모 파악:
text
service:cupixworks-any-potree-*-agent "InvalidState" status:warn

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial — throw new Error(...) → 원본 에러 직접 throw로 변경하는 1줄 수정. 경쟁 조건 방지는 standard 복잡도.