TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, co
RCA: TransferManager::downloadFile - 404 Not Found on pano download
Error Log#
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:37—SingleshotService.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
# 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 검사를 수행하지 않는다.
# 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) 반환:
# 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 생성:
# 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_option이 initial_revision + 1로 version 1을 계산하여 존재하지 않는 S3 객체에 대한 presigned URL을 생성했다.
Log Evidence#
Singleshot agent의 실행 흐름:
Datadog query: service:cupixworks-capture-singleshot-agent "678511"
Time range: 2026-04-09T19:00:00Z to 2026-04-09T22:30:00Z
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 응답:
Datadog query: service:cupixworks-api "panos/80668710/download"
Time range: 2026-04-09T20:00:00Z to 2026-04-09T22:30:00Z
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 로그):
Datadog query: service:cupixworks-capture-singleshot-agent "80668710" "CupixAuth"
{
"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— 원본 이미지 크기 0stat.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 쿼리:
service:cupixworks-capture-singleshot-agent status:error "TransferManager::downloadFile" "404"
service:cupixworks-api "PanosController#download_single_resource" status:info
resource_state: "created"인 pano에 대한 download 요청을 추적하는 custom metric 또는 로그 추가 권장
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard