ES /docs

CupixAuth::handleError - unknown error: "{\"statusCode\":404,\"headers\":{\"x-amz-request-id\":\"308

RCA: CupixAuth::handleError - S3 404 on pano original download

Error Log#

Datadog Logs

json
{
  "statusCode": 404,
  "headers": { "server": "AmazonS3" },
  "request": {
    "uri": {
      "pathname": "/cupixworks-source-dc9dcff32488-usea1/resources/oty8x8/usea1/v1",
      "href": "https://s3.amazonaws.com/cupixworks-source-dc9dcff32488-usea1/resources/oty8x8/usea1/v1?..."
    },
    "method": "GET",
    "headers": {
      "X-CUPIX-AUTH": "session_token:xbf6c117sgvb,session_id:10490898",
      "referer": "http://api-tesla.cupix.internal/api/v1/panos/80668710/download?original=true"
    }
  }
}

Impact#

  • Service: cupixworks-capture-singleshot-agent
  • 발생 횟수: 1
  • 최초 발생: 2026-04-09T21:20:43.280Z
  • 최근 발생: 2026-04-09T21:20:43.280Z

Root Cause Summary#

Singleshot agent가 capture 678511의 pano 80668710 원본 이미지를 다운로드하기 위해 GET /api/v1/panos/80668710/download?original=true를 호출했다. Tesla API는 get_revision_by_download_option에서 initial_revision(0) + 1 = 1로 revision을 계산하고, S3 presigned URL(resources/oty8x8/usea1/v1)로 302 redirect를 반환했다. 그러나 해당 S3 경로에 실제 오브젝트가 존재하지 않아 S3가 404를 반환했다. API는 presigned URL 생성 시 S3 오브젝트 존재 여부를 검증하지 않으며, pano resource의 revision 상태(v1에 실제 업로드되었는지)도 확인하지 않는다. Agent는 5회 재시도 후 최종 실패했고, CupixAuth::handleError에서 S3 응답을 "unknown error"로 로깅했다.

Technical Analysis#

Code Path#

  1. Entry point — Singleshot agent가 SQS 메시지로 capture 678511 처리 시작:
typescript
// cupixworks/applications/agents/packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:37-44
protected run = async (targetId: number, msgObject: any): Promise<void> => {
    const serverCapture = await this.getCaptureById(targetId);
    if (serverCapture) {
        await this.updateCaptureSingleshotState(targetId, TESLA.UpdateCaptureRequest.SingleshotStateEnum.Running);
        await this.runAlignScript(serverCapture);  // <-- pano 다운로드 여기서 발생
        await this.uploadStitchedPano(targetId);
        await this.updateCaptureSingleshotState(targetId, TESLA.UpdateCaptureRequest.SingleshotStateEnum.Stopped);
    }
};
  1. Align module에서 pano 다운로드 — original=true로 요청:
typescript
// cupixworks/applications/agents/packages/cupix-tesla-singleshot-agent/src/align/align.module.ts:61-62
const downloadUrl = `${cupixAuth.apiUrl}/panos/${panoId}/download?original=true`;
await downloadFile(downloadUrl, localFilePath, { 'X-CUPIX-AUTH': cupixAuth.accessToken });

Agent는 항상 original=true 파라미터로 pano를 다운로드한다. 이것은 "원본 해상도" 이미지를 요청하는 것이다.

  1. Tesla API — revision 계산 및 presigned URL 생성:
ruby
# tesla/app/controllers/concerns/single_resourcable/panos_controller.rb:7-11
def download_single_resource
  stitched, original = parse_download_params
  redirect_to @resource.download_url(ver: @model.get_revision_by_download_option([stitched, original])), allow_other_host: true
end
ruby
# tesla/app/models/concerns/resourcable/pano.rb:106-118
def get_revision_by_download_option(options = [])
  stitched = options[0]
  original = options[1]
  if original == true
    self.initial_revision + 1  # initial_revision = 0 → revision = 1
  else
    nil
  end
end

original=true일 때 initial_revision + 1 = 1을 반환한다. initial_revisionsys[:initial_revision]에서 가져오며, 기본값은 0이다 (revisionable.rb:35-36).

  1. Presigned URL 생성 — S3 오브젝트 존재 여부 확인 없음:
ruby
# tesla/app/models/concerns/storagable/resource.rb:185-205
def download_url(opts = {})
  ver = opts[:ver] || self.revision
  raise Cupix::Errors::Resource.new(code: 'ENT10011', reason: "Resource does not uploaded: #{ver}") if ver.zero?
  # ver = 1이므로 zero? 체크를 통과
  self.object(ver).presigned_url(:get, expires_in: exp, response_content_disposition: "filename=#{...}")
end

ver.zero? 체크만 있어서 ver = 1은 통과한다. S3 오브젝트가 실제로 존재하는지는 확인하지 않는다.

  1. S3 key 생성:
ruby
# tesla/app/models/concerns/decorators/resource.rb:5-10
def object_key(ver = nil, s3_region_code: nil)
  _ver = ver.presence || self.revision
  _s3_region_code = s3_region_code.presence || storage_option.s3_region_code
  "resources/#{key}/#{_s3_region_code}/v#{_ver}"
end

결과: resources/oty8x8/usea1/v1 — 이 경로에 S3 오브젝트가 존재하지 않는다.

  1. Failure point — Agent download에서 S3 404 수신:

배포된 코드에서 TransferManager::downloadFile이 S3 redirect를 따라가서 404를 받고, CupixAuth::handleError를 호출하여 에러를 로깅한다. 현재 repo의 align.module.ts에는 TransferManager가 사용되지 않으므로, 배포 버전은 repo보다 이전 코드일 수 있다.

typescript
// cupixworks/applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer.manager.ts:67-74
sendReq.on('response', res => {
    if (res.statusCode === 200) {
        sendReq.pipe(fileStream);
    } else {
        logger.error('TransferManager::downloadFile | response path: %s, code: %d, message: %s', path, res.statusCode, res.statusMessage);
        reject(this.cupixAuth.handleError(res));  // S3 404 응답을 handleError에 전달
    }
});
typescript
// cupixworks/applications/agents/packages/api/src/authentication/cupix-auth.ts:42-57
handleError = (ec: any): any => {
    const response = ec && CPUtils.isJsonString(ec) ? JSON.parse(ec) : ec.response;
    if (response != undefined) {
        // ...
    } else {
        logger.warn('CupixAuth::handleError | Undefined response: %s', JSON.stringify(ec, ...));
    }
    return ec;
};

S3의 404 응답 객체는 JSON string이 아니고 .response 속성도 없으므로, responseundefined가 되어 "Undefined response" 브랜치로 진입한다. 로그에 보이는 "unknown error" 메시지는 배포 버전의 코드 차이에 의한 것이다.

Log Evidence#

Datadog 검색 쿼리:

text
service:cupixworks-capture-singleshot-agent status:error
Time range: 2026-04-09T20:20:43Z to 2026-04-09T21:50:43Z

전체 에러 타임라인 (capture 678511, pano 80668710):

시간 (KST) 이벤트
06:13:38 Singleshot agent invoked for capture 678511
06:19:48 `BaseService::runByMessage
06:19:49 SingleshotService::runAlignScript 실행
06:19:51 첫 번째 다운로드 시도 — TransferManager::downloadFile 404
06:20:02 두 번째 재시도 — 404
06:20:12 세 번째 재시도 — 404
06:20:22 네 번째 재시도 — 404
06:20:33 다섯 번째 재시도 — 404
06:20:43 TransferManager::failTask — count: 5/5, 최종 실패
06:20:43 CupixAuth::handleError — S3 404 응답 로깅
06:20:46 BaseService::cleanUpAnythingRelatedModel — workspace 정리
06:20:46 pano_postprocessor_state → queued (후속 처리 시작)
06:24:23 pano_postprocessor_state → error (후속 처리도 실패)

Agent 에러 로그 (5회 재시도 실패):

text
TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
TransferManager::failTask - path: /tmp/workspace/678511/original_panos/80668710.jpg, url: http://api-tesla.cupix.internal/api/v1/panos/80668710/download?original=true, count: 5/5

API 로그 — pano 80668710에 대한 기록:

text
service:cupixworks-api 80668710
Time range: 2026-04-09T19:00:00Z to 2026-04-09T21:19:00Z

결과: 0건 — singleshot agent가 다운로드를 시도하기 전에 pano 80668710에 대한 API 호출이 없었다. 즉, 이 pano의 원본 이미지가 v1 revision으로 업로드된 적이 없을 가능성이 높다.

API 302 응답 (6회 — agent 재시도 포함):

text
[302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)

API는 매번 정상적으로 302 redirect를 반환했으나, redirect 대상인 S3 presigned URL의 오브젝트가 존재하지 않았다.

Fix Recommendation#

즉시 조치 (Critical)#

파일: tesla/app/models/concerns/storagable/resource.rb:185-205 (download_url 메서드)

현재 ver.zero? 체크만 있는데, revision이 0보다 큰 경우에도 해당 S3 오브젝트가 실제로 업로드되었는지 검증해야 한다. download_url에서 self.object(ver).exists?를 호출하여 S3 오브젝트 존재 여부를 확인하고, 없으면 적절한 에러(ENT10011 등)를 raise해야 한다.

또는, get_revision_by_download_option에서 반환하는 revision이 실제 resource의 revision 값을 초과하지 않는지 확인하는 방법도 있다.

단기 개선 (1주 이내)#

  1. Singleshot agent에서 download 실패 시 graceful fallback 추가: original=true 다운로드가 404로 실패하면 original=false (현재 revision)로 fallback하여 다운로드를 재시도하는 로직이 필요하다. 파일: cupixworks/applications/agents/packages/cupix-tesla-singleshot-agent/src/align/align.module.ts:61-62

  2. CupixAuth::handleError에서 S3 에러 응답을 구조화된 형태로 파싱: 현재 S3 응답은 "unknown error"로 로깅되어 디버깅이 어렵다. S3 XML 에러 응답에서 <Code>, <Message> 필드를 추출하여 로깅하면 진단 가능성이 향상된다.

장기 개선 (재발 방지)#

  1. Resource upload 상태 추적 강화: 현재 revision 숫자만 관리하고, 각 revision의 S3 업로드 완료 여부를 별도로 추적하지 않는다. Resource 모델에 각 revision별 upload 상태를 기록하여, 업로드되지 않은 revision에 대한 download 요청을 API 레벨에서 차단해야 한다.

  2. Presigned URL 생성 전 S3 HEAD 체크: download_url 호출 시 S3 HeadObject를 먼저 수행하여 오브젝트 존재 여부를 확인하면, 클라이언트에게 404 대신 명확한 에러 메시지를 전달할 수 있다. 단, 성능 영향을 고려하여 캐싱 전략이 필요하다.

Monitoring#

  • Singleshot agent의 S3 404 에러 모니터링:
text
service:cupixworks-capture-singleshot-agent status:error "statusCode\":404"
  • API에서 download_single_resource 호출 후 S3 404 발생 패턴 추적:
text
service:cupixworks-capture-singleshot-agent "TransferManager::failTask"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 발생 빈도 1회로, 특정 capture(678511)의 pano 원본 이미지가 S3에 없는 데이터 상태 문제이다. 그러나 API에서 S3 오브젝트 존재 여부를 검증하지 않는 구조적 문제가 있어, 다른 capture에서도 동일 패턴이 발생할 수 있다.