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#
- 19:30:22 UTC — Agent가 SQS 메시지를 수신하여 pointcloud 1054099 처리 시작 (
potree_state: created) - 19:36:18 UTC — 동일 pointcloud에 대한 두 번째 처리 시도 시작 (
potree_state: uploading) - 19:37-19:39 UTC — PotreeConverter 실행 (두 시도 모두)
- 19:40:34.195 UTC — 두 번째 시도에서
potree_upload_credentialsAPI 호출 시 HTTP 400 반환 (InvalidState: potree_state is uploaded state) - 19:40:34.196 UTC —
getApiErrorToDeleteMessage에서"undefined response"warn 로그 - 19:40:34.230 UTC — SQS 메시지 정상 삭제
- 19:41:04.540 UTC —
handlingMessageErrors에서 최종 error 로그 기록 - 19:56:45 UTC — workspace 정리 완료
Error Log#
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_state를 error로 업데이트하려 시도했을 가능성이 있어, 이미 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)
try {
await this.runByMessages();
} catch (error) {
await this.handlingMessageErrors(error);
}
runByMessage(potree-service.ts:180-226)에서 pointcloud의potreeState가Uploaded가 아닌 경우 변환 + 업로드 파이프라인 실행
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
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를 호출
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.response가undefined이므로'undefined response'반환
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';
}
error는 new Error("PotreeService::uploadPotreeFiles - [object Object]") 형태의 Error 객체이므로:
CPUtils.isJsonString(error)→false(Error 객체는 JSON 문자열이 아님)error.response→undefined(표준 Error 객체에는.response가 없음)- 결과:
'undefined response'반환
추가 문제: handlingMessageErrors에서 apiErrorObject가 'undefined response' (truthy)이므로 updatePotreeState(id, Error)를 호출하여 이미 uploaded 상태인 pointcloud의 potree_state를 error로 변경할 수 있다 (line 517).
Log Evidence#
Datadog 검색 쿼리:
service:cupixworks-any-potree-r1-xlarge-agent status:* @environment:production
핵심 로그 시퀀스 (pointcloud 1054099):
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 에러 로그:
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"}
19:40:34.196 [warn] PotreeService::getApiErrorToDeleteMessage | undefined response - {}
JSON.stringify(error) 결과가 {}인 것은 Error 객체의 standard properties(message, stack)가 non-enumerable이기 때문이다. 이는 new Error(...) 래핑의 직접적 증거이다.
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"패턴의 에러를 모니터링하여 에러 래핑 수정 후 재발 여부 확인:
service:cupixworks-any-potree-*-agent "undefined response" status:error
InvalidStateAPI 에러 빈도를 모니터링하여 경쟁 조건의 규모 파악:
service:cupixworks-any-potree-*-agent "InvalidState" status:warn
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial —
throw new Error(...)→ 원본 에러 직접 throw로 변경하는 1줄 수정. 경쟁 조건 방지는 standard 복잡도.