ES /docs

TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, co

RCA: TransferManager::downloadFile - 404 Not Found on pano download

Error Log#

Datadog Logs

text
TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found

Impact#

  • Service: cupixworks-capture-singleshot-agent
  • 발생 횟수: 6
  • 최초 발생: 2026-04-09T21:19:51.916Z
  • 최근 발생: 2026-04-09T21:20:43.279Z

Root Cause Summary#

Singleshot agent가 capture 678511의 pano 80668710 원본 이미지를 다운로드하려 했으나, 해당 pano의 이미지가 S3에 업로드된 적이 없어 404 오류가 발생했다. DB에 pano 레코드는 존재하지만 resource_state: "created", filesize: null 상태로 업로드가 완료되지 않았다. Rails API의 PanosController#download_single_resource가 base controller의 @resource.revision == 0 검증을 우회하고, get_revision_by_download_option으로 initial_revision + 1 = 1을 계산하여 존재하지 않는 S3 객체에 대한 presigned URL을 생성한 것이 직접적인 원인이다.

Technical Analysis#

Code Path#

  • Entry point: cupixworks/applications/agents/packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:37SingleshotService.run() 호출
  • SingleshotService.runAlignScript() 에서 runAlign() 호출 → pano 다운로드 시도
  • API 요청: GET /api/v1/panos/80668710/download?original=true
  • Controller: tesla/app/controllers/concerns/single_resourcable/panos_controller.rb:7
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

이 메서드는 base controller인 SingleResourcableController#download_single_resource를 override하며, base에 있는 @resource.revision == 0 검사를 수행하지 않는다.

ruby
# tesla/app/controllers/concerns/single_resourcable_controller.rb:10-19
def download_single_resource
  if @resource.revision == 0
    raise Cupix::Errors::Resource.new(code: 'ENT10011', reason: "Resource does not uploaded: #{@resource.revision}")
  else
    download_opts = {}
    download_opts[:filename] = params[:filename] if params[:filename].present?

    redirect_to @resource.download_url(download_opts), allow_other_host: true
  end
end
  • get_revision_by_download_option에서 original=true일 때 self.initial_revision + 1 (= 0 + 1 = 1) 반환:
ruby
# tesla/app/models/concerns/resourcable/pano.rb:113-117
if original == true
  self.initial_revision + 1
else
  nil
end
  • download_url(ver: 1)에서 ver.zero? 검사 통과 (1은 0이 아님), presigned URL 생성:
ruby
# tesla/app/models/concerns/storagable/resource.rb:185-203
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?
  # ...
  self.object(ver).presigned_url(:get, expires_in: exp, response_content_disposition: "filename=#{CGI.escape(filename) rescue nil}")
end
  • Failure point: S3에서 presigned URL로 접근 시 해당 객체가 존재하지 않아 404 반환

기대 동작: pano 이미지가 아직 업로드되지 않은 경우(resource_state: "created", revision: 0), API가 다운로드 요청을 거부해야 한다 (base controller처럼 ENT10011 에러 반환).

실제 동작: PanosController의 override가 revision 검사를 건너뛰고, get_revision_by_download_optioninitial_revision + 1로 version 1을 계산하여 존재하지 않는 S3 객체에 대한 presigned URL을 생성했다.

Log Evidence#

Singleshot agent의 실행 흐름:

text
Datadog query: service:cupixworks-capture-singleshot-agent "678511"
Time range: 2026-04-09T19:00:00Z to 2026-04-09T22:30:00Z
text
06:19:48 KST [info]  BaseService::runByMessage | id: 678511
06:19:48 KST [info]  SingleshotService::getCaptureById | captureId: 678511
06:19:49 KST [info]  SingleshotService::runAlignScript | params: {"workspace":"/tmp/workspace/678511","env_name":"production","api_endpoint":"http://api-tesla.cupix.internal/api/v1","session_id":10490898,"capture_id":678511,"team_id":200}
06:19:51 KST [error] TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
06:20:02 KST [error] TransferManager::downloadFile - response path: ... code: 404 (retry 2)
06:20:12 KST [error] TransferManager::downloadFile - response path: ... code: 404 (retry 3)
06:20:22 KST [error] TransferManager::downloadFile - response path: ... code: 404 (retry 4)
06:20:33 KST [error] TransferManager::downloadFile - response path: ... code: 404 (retry 5)
06:20:43 KST [error] TransferManager::downloadFile - response path: ... code: 404 (retry 6 - final)
06:20:43 KST [error] 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
06:20:46 KST [info]  BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/678511

API 측 로그 — 모든 요청에 302 redirect 응답:

text
Datadog query: service:cupixworks-api "panos/80668710/download"
Time range: 2026-04-09T20:00:00Z to 2026-04-09T22:30:00Z
text
06:19:52 KST [info] [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)
06:20:03 KST [info] [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)
06:20:12 KST [info] [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)
06:20:24 KST [info] [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)
06:20:33 KST [info] [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)
06:20:43 KST [info] [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)

S3 404 상세 에러 (CupixAuth::handleError 로그):

text
Datadog query: service:cupixworks-capture-singleshot-agent "80668710" "CupixAuth"
json
{
  "statusCode": 404,
  "headers": {
    "server": "AmazonS3",
    "x-amz-request-id": "308Q51CF0EVPZ140"
  },
  "request": {
    "uri": {
      "hostname": "s3.amazonaws.com",
      "pathname": "/cupixworks-source-dc9dcff32488-usea1/resources/oty8x8/usea1/v1"
    },
    "method": "GET",
    "headers": {
      "referer": "http://api-tesla.cupix.internal/api/v1/panos/80668710/download?original=true"
    }
  }
}

DB 상태 확인 (Kibana):

Pano 80668710:

  • state: "created", resource_state: "created", filesize: null
  • 이미지가 S3에 업로드된 적 없음

Capture 678511:

  • state: "done", method: "singleshot", singleshot_state: "stopped"
  • stat.size.pano_original_images: 0 — 원본 이미지 크기 0
  • stat.uploading.finished_at: "2026-04-09T21:13:37.075Z" — 업로드 완료 보고됨
  • processing_status: "run_aws_task", editing_state: "escalated"

Fix Recommendation#

즉시 조치 (Critical)#

tesla/app/controllers/concerns/single_resourcable/panos_controller.rb:7-11에서 다운로드 전 resource의 실제 revision/state를 검증해야 한다. @resource.revision == 0이거나 @resource.state != 'done'인 경우 presigned URL을 생성하지 않고 에러를 반환하도록 수정한다. Base controller의 revision 검사 로직을 pano override에서도 유지하는 것이 핵심이다.

단기 개선 (1주 이내)#

  • download_url 메서드(tesla/app/models/concerns/storagable/resource.rb:185)에서 ver.zero? 외에도 S3 객체 존재 여부 또는 resource_state를 확인하는 방어 로직을 추가한다. ver > 0이더라도 실제 업로드가 완료되지 않은 경우가 있으므로, revision만으로는 충분하지 않다.
  • Singleshot agent 측(TransferManager)에서 404 에러에 대한 retry를 하지 않도록 checkStatusCode 로직을 검토한다. 현재 코드는 error.statusCode > 400 && error.statusCode < 500일 때 retry를 건너뛰도록 되어 있으나, 실제로는 5회 retry가 발생했다. handleError가 반환하는 error 객체의 statusCode 프로퍼티 전파 경로를 확인해야 한다.

장기 개선 (재발 방지)#

  • Pano 생성 후 업로드 완료까지의 상태 전이를 보다 엄격하게 관리한다. resource_state: "created"인 pano에 대해 processing pipeline이 시작되지 않도록 capture의 processing trigger 조건에 pano upload 완료 여부 검증을 추가한다.
  • Upload 완료되지 않은 리소스에 대한 download API 호출 시 명확한 에러 응답(예: 409 Conflict 또는 404)을 반환하여, downstream 서비스가 의미 있는 에러 처리를 할 수 있도록 한다.

Monitoring#

  • 추가할 메트릭: pano download API에서 presigned URL redirect 후 S3 404가 발생하는 비율
  • Datadog 쿼리:
text
service:cupixworks-capture-singleshot-agent status:error "TransferManager::downloadFile" "404"
text
service:cupixworks-api "PanosController#download_single_resource" status:info
  • resource_state: "created"인 pano에 대한 download 요청을 추적하는 custom metric 또는 로그 추가 권장

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard