ES /docs

TransferManager::uploadNewPointclouds | {"stack":"HttpError: HTTP request failed

RCA: TransferManager::uploadNewPointclouds | HttpError 403

Error Log#

Datadog Logs

text
TransferManager::uploadNewPointclouds | {"stack":"HttpError: HTTP request failed
    at Request._callback (/tmp/agent/dist/node_modules/@tesla/typescript-node-sdk/api/pointcloudApi.js:874:40)
    at self.callback (/tmp/agent/dist/node_modules/request/request.js:185:22)
    at Request.emit (node:events:524:28)
    at Request.emit (node:domain:489:12)
    at Request.<anonymous> (/tmp/agent/dist/node_modules/request/request.js:1154:10)
    at Request.emit (node:events:524:28)
    at Request.emit (node:domain:489:12)
    at IncomingMessage.<anonymous> (/tmp/agent/dist/node_modules/request/request.js:1076:12)
    at Object.onceWrapper (node:events:638:28)
    at IncomingMessage.emit (node:events:536:35)","message":"HTTP request failed","response":{"body":{},"statusCode":403},"body":{},"statusCode":403,"name":"HttpError"}

Impact#

  • Service: cupixworks-capture-3dreconstruction-instance
  • Team: southlandind
  • 발생 횟수: 1
  • 최초 발생: 2026-04-13T02:56:06.117Z
  • 최근 발생: 2026-04-13T02:56:06.117Z

Root Cause Summary#

3D Reconstruction agent가 약 2시간 23분간 처리 작업을 수행한 후 결과 pointcloud를 업로드하려고 POST /api/v1/pointclouds (create) API를 호출했으나, 요청 파라미터에 포함된 cluster_id에 해당하는 Cluster 레코드를 Tesla API 서버에서 찾지 못해 Cupix::Errors::NotFound (코드: ARG10002, 이유: Cluster not found) 예외가 발생했습니다. Tesla의 error handler가 NotFound 예외를 HTTP 403으로 매핑하여 반환하기 때문에, agent 측에서는 403 Forbidden으로 인식되었습니다. Cluster가 삭제되었거나, agent의 세션 사용자에게 해당 cluster에 대한 접근 권한이 없어 permission_joins 쿼리에서 조회되지 않았을 가능성이 있습니다.

Technical Analysis#

Code Path#

1. Agent 진입점: TransferManager::uploadNewPointclouds

  • Entry point: cupixworks/applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer.manager.ts:265
typescript
// transfer.manager.ts:265-277
uploadNewPointclouds = (cpCluster: CPCluster, items: CPPointcloud[]): Promise<void> => new Promise((resolve, reject) => {
    const container = new UploadNewPointcloudsContainer(this, cpCluster, items, resolve, reject);
    container.init()
        .then(() => {
            container.tasks.forEach(task => {
                this.addTask(task);
            });
        })
        .catch(ec => {
            logger.error('TransferManager::uploadNewPointclouds | %s', JSON.stringify(ec, Object.getOwnPropertyNames(ec)));
            reject();
        });
});

UploadNewPointcloudsContainer.init()가 호출되면 먼저 group pointcloud를 생성하고, 이후 각 sub pointcloud에 대한 task를 생성합니다. init() 중 에러 발생 시 .catch에서 로그를 기록하고 reject합니다.

2. Container 초기화: group pointcloud 생성

  • cupixworks/applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer/upload-new-pointclouds.container.ts:26-38
typescript
// upload-new-pointclouds.container.ts:26-38
private createGroupPointcloud = async (): Promise<void> => {
    const cpCapture = this.cpCluster.cpCapture;
    const createPointcloudRequest = {
        kind: 'group',
        name: cpCapture?.name + '_' + this.cpCluster.name,
        pointcloud_type: TESLA.PointcloudType._3dReconstructed,
        level_id: cpCapture?.srvLevel?.id as number,
        record_id: cpCapture?.srvRecord?.id as number,
        capture_id: cpCapture?.id as number,
        cluster_id: this.cpCluster?.id as number
    };
    this._groupPointcloud = await this.cupixApi.pointcloud.create(createPointcloudRequest);

cupixApi.pointcloud.create()를 호출하여 POST /api/v1/pointclouds로 요청합니다. 이 요청에 cluster_id가 포함됩니다.

3. SDK 레벨: pointcloudApi.createPointcloud

  • cupix-api/client/openapi/typescript-node/tesla-v1/api/pointcloudApi.ts:863-932
typescript
// pointcloudApi.ts:924-932
localVarRequest(localVarRequestOptions, (error, response, body) => {
    if (error) {
        reject(error);
    } else {
        body = ObjectSerializer.deserialize(body, "PointcloudResponse");
        if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) {
            resolve({ response: response, body: body });
        } else {
            reject(new HttpError(response, body, response.statusCode));
        }

서버가 403을 반환하면 new HttpError(response, body, 403)로 reject됩니다. 이것이 스택 트레이스의 pointcloudApi.js:874 에러입니다 (컴파일된 JS의 라인 번호).

4. Tesla API 서버: PointcloudsController#createPointcloudFactory#create!

  • Failure point: tesla/app/factories/pointcloud_factory.rb:35
ruby
# pointcloud_factory.rb:28-36
if params[:pointcloud_type] == '3d_reconstructed'
  raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'capture_id is required') if params[:capture_id].nil?
  raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'cluster_id is required') if params[:cluster_id].nil?

  _capture = CaptureRepository.new(current_user: self.current_user).show(params[:capture_id])
  raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'capture.record_id and record_id are different') if _capture.record_id != self.model.record_id

  _cluster = ClusterRepository.new(current_user: self.current_user).show(params[:cluster_id])  # ← 여기서 실패
  raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'cluster.record_id and record_id are different') if _cluster.record_id != self.model.record_id

ClusterRepository.show(params[:cluster_id])가 호출되면 BaseRepository.show에서 해당 cluster를 조회합니다.

5. BaseRepository.show — NotFound 발생 지점

  • tesla/app/repositories/base_repository.rb:327-351
ruby
# base_repository.rb:327-351
query =
  if skip_permission || current_user == ::User.unauthorized_user
    where(attrs)
  elsif current_user.present?
    permission_joins(default_joins(current_class), current_user).where(attrs)
  end

scope = current_class.visibility_scope(visibility)
model = query.merge(scope).first

# admin 팀이면 권한 무시하고 재조회
if model.nil? && current_user.present? && current_user.team.domain == 'admin'
  # ...
end

if model.nil?
  if self.where(attrs).in_trash.present?
    raise Cupix::Errors::NotFound.new(code: 'ENT4000', reason: "#{current_class.name} not found")
  else
    raise Cupix::Errors::NotFound.new(code: 'ARG10002', reason: "#{current_class.name} not found")
  end
end

permission_joins를 포함한 쿼리에서 cluster를 찾지 못하면 Cupix::Errors::NotFound가 발생합니다. 에러 코드 ARG10002와 reason "Cluster not found"가 Datadog 로그와 정확히 일치합니다.

6. NotFound → HTTP 403 매핑

  • tesla/app/controllers/concerns/client_error_controller.rb:27,55-57
ruby
# client_error_controller.rb:27
rescue_from Cupix::Errors::NotFound, with: :not_found_403_error

# client_error_controller.rb:55-57
def not_found_403_error(exception)
  raise_error(403, exception)
end

Tesla API는 보안상의 이유로 NotFound 에러를 404가 아닌 403으로 반환합니다. 이로 인해 agent 측에서는 단순한 "Permission Denied"로 보이지만, 실제 원인은 cluster 레코드가 조회되지 않은 것입니다.

Log Evidence#

Datadog 쿼리 1: Agent 에러 로그

text
service:cupixworks-capture-3dreconstruction-instance status:error
Time: 2026-04-13T01:56:06Z to 2026-04-13T03:26:06Z

2건의 에러 로그 발견:

json
{
  "timestamp": "2026-04-13T02:56:06.117Z",
  "status": "error",
  "host": "0ff003130516",
  "session": "a0e52065a19df4c7719cc124bdcc7c5d41e8ea72",
  "capture_id": "678250",
  "job_id": "1010867",
  "team": "southlandind",
  "user": "dylan.gray@southlandind.com",
  "message": "TransferManager::uploadNewPointclouds | HttpError: HTTP request failed ... statusCode: 403"
}
text
2026-04-13T02:56:06.117Z [error] ThreeDReconstruction::run | end

Datadog 쿼리 2: Tesla API 403 응답 로그

text
service:cupixworks-api @http.status_code:403
Time: 2026-04-13T02:55:00Z to 2026-04-13T02:57:00Z

핵심 로그 (정확히 일치하는 403 요청):

json
{
  "timestamp": "2026-04-13T02:56:06.253Z",
  "http_status": 403,
  "http_method": "POST",
  "url_path": "/api/v1/pointclouds",
  "controller": "Api::V1::PointcloudsController",
  "action": "create",
  "user_id": 38580,
  "user_email": "dylan.gray@southlandind.com",
  "team": "southlandind (ID: 691)",
  "error_code": "ARG10002",
  "error_reason": "Cluster not found",
  "error_class": "Cupix::Errors::NotFound",
  "auth_method": "COGNITO",
  "user_agent": "cupix-agent",
  "params": "record_id: 124469, level_id: 54822, capture_id: 678250",
  "duration": "49.95ms",
  "request_id": "fa38719d-6135-44b7-93fb-6d9435499296"
}

Datadog 쿼리 3: Job 실행 타임라인

text
service:cupixworks-capture-3dreconstruction-instance @team.domain:southlandind
Time: 2026-04-12T00:00:00Z to 2026-04-13T23:59:59Z

Job 1010867 (Capture 678250) 실행 흐름:

시각 (UTC) 이벤트
00:32:44.521 ThreeDReconstruction::init
00:32:44.521 ThreeDReconstruction::authenticate | begin
00:32:44.616 CupixAuth::setSession | session_id: a0e52065...
00:32:44.616 ThreeDReconstruction::run | begin
00:32:44.617 JobManager::loadJob | begin - job id: 1010867
00:32:45.080 ThreeDReconstruction::loadVideos | video count: 2
00:32:45.139 ThreeDReconstruction::loadClusters | cluster count: 2
00:33:00.035 runThreeDReconstruction environments | domain: southlandind
(~2시간 23분 처리)
02:56:06.117 TransferManager::uploadNewPointclouds | HttpError 403
02:56:06.117 ThreeDReconstruction::run | end
02:56:06.748 terminateService | force shutdown after 10 seconds

Datadog 쿼리 4: 동일 기간 다른 southlandind 작업

text
service:cupixworks-capture-3dreconstruction-instance @team.domain:southlandind
Time: 2026-04-12T00:00:00Z to 2026-04-13T23:59:59Z

동일 기간 southlandind 팀의 다른 10건 이상의 작업은 모두 정상 완료됨. 이는 팀 전체의 권한 문제가 아니라 이 특정 cluster의 상태 문제임을 시사합니다.

Datadog 쿼리 5: 7일간 동일 에러 패턴 검색

text
service:cupixworks-capture-3dreconstruction-instance ("uploadNewPointclouds" OR "pointcloudApi") status:error
Time: 최근 7일

1건만 발견 (본 건). 반복 패턴이 아닌 단발성 이슈입니다.

Fix Recommendation#

즉시 조치 (Critical)#

  • 데이터 확인 필요: Capture 678250에 연결된 cluster의 현재 상태를 DB에서 확인해야 합니다. Cluster가 삭제(trashed)되었거나 다른 사용자/프로세스에 의해 변경되었을 가능성이 있습니다. 에러 코드가 ARG10002 (Cluster not found)이지 ENT4000 (trashed)이 아니므로 permission_joins 쿼리에서 조회되지 않은 것으로 보입니다.
  • 재처리 가능 여부 확인: 해당 Job 1010867을 재실행할 수 있는지 확인하고, cluster 상태가 정상이면 재처리합니다.

단기 개선 (1주 이내)#

  • Agent 측 에러 메시지 개선: transfer.manager.ts:273-274에서 에러를 로깅할 때 response body가 비어있어({}) 실제 에러 원인(Cluster not found)을 알 수 없습니다. Tesla API가 response body에 에러 코드와 reason을 포함하도록 확인하고, agent 측에서 이를 파싱하여 로깅하도록 개선해야 합니다.
  • 사전 검증 로직 추가: UploadNewPointcloudsContainer.createGroupPointcloud() 호출 전에 cluster가 여전히 유효한지 API로 확인하는 pre-check를 추가하면, 2시간 이상의 처리 시간 낭비를 방지할 수 있습니다.

장기 개선 (재발 방지)#

  • Long-running job의 리소스 유효성 재검증: 3D reconstruction처럼 수 시간 걸리는 작업에서는 처리 완료 후 업로드 단계 진입 전에 관련 리소스(cluster, capture, record)의 유효성을 재확인하는 패턴을 도입해야 합니다.
  • Tesla API의 NotFound → 403 매핑 개선: client_error_controller.rb:27에서 NotFound를 403으로 매핑하는 설계는 보안 목적이지만, API 클라이언트(특히 내부 agent)가 디버깅하기 어렵게 만듭니다. 내부 agent 요청에 대해서는 더 구체적인 에러 정보(에러 코드, reason)를 response body에 포함하는 것을 검토할 수 있습니다.

Monitoring#

  • 3D Reconstruction agent의 upload 단계 실패를 추적하는 모니터 추가:
text
service:cupixworks-capture-3dreconstruction-instance status:error "TransferManager::uploadNewPointclouds"
  • Tesla API의 ARG10002 (Not Found) 에러 빈도 추적:
text
service:cupixworks-api @http.status_code:403 @error.code:ARG10002 @http.url_details.path:/api/v1/pointclouds

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 단발성 이슈(7일간 1건)이며 다른 팀/사용자에게 영향 없음. 특정 cluster의 데이터 상태 문제로 추정되며, 시스템 전체 장애 위험은 낮음.