CupixAuth::handleError - unknown error: "{\"statusCode\":404,\"headers\":{\"x-amz-request-id\":\"308
RCA: CupixAuth::handleError - S3 404 on pano original download
Error Log#
{
"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#
- Entry point — Singleshot agent가 SQS 메시지로 capture 678511 처리 시작:
// 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);
}
};
- Align module에서 pano 다운로드 —
original=true로 요청:
// 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를 다운로드한다. 이것은 "원본 해상도" 이미지를 요청하는 것이다.
- Tesla API — revision 계산 및 presigned URL 생성:
# 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
# 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_revision은 sys[:initial_revision]에서 가져오며, 기본값은 0이다 (revisionable.rb:35-36).
- Presigned URL 생성 — S3 오브젝트 존재 여부 확인 없음:
# 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 오브젝트가 실제로 존재하는지는 확인하지 않는다.
- S3 key 생성:
# 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 오브젝트가 존재하지 않는다.
- Failure point — Agent download에서 S3 404 수신:
배포된 코드에서 TransferManager::downloadFile이 S3 redirect를 따라가서 404를 받고, CupixAuth::handleError를 호출하여 에러를 로깅한다. 현재 repo의 align.module.ts에는 TransferManager가 사용되지 않으므로, 배포 버전은 repo보다 이전 코드일 수 있다.
// 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에 전달
}
});
// 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 속성도 없으므로, response는 undefined가 되어 "Undefined response" 브랜치로 진입한다. 로그에 보이는 "unknown error" 메시지는 배포 버전의 코드 차이에 의한 것이다.
Log Evidence#
Datadog 검색 쿼리:
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회 재시도 실패):
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에 대한 기록:
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 재시도 포함):
[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주 이내)#
-
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 -
CupixAuth::handleError에서 S3 에러 응답을 구조화된 형태로 파싱: 현재 S3 응답은 "unknown error"로 로깅되어 디버깅이 어렵다. S3 XML 에러 응답에서<Code>,<Message>필드를 추출하여 로깅하면 진단 가능성이 향상된다.
장기 개선 (재발 방지)#
-
Resource upload 상태 추적 강화: 현재 revision 숫자만 관리하고, 각 revision의 S3 업로드 완료 여부를 별도로 추적하지 않는다. Resource 모델에 각 revision별 upload 상태를 기록하여, 업로드되지 않은 revision에 대한 download 요청을 API 레벨에서 차단해야 한다.
-
Presigned URL 생성 전 S3 HEAD 체크:
download_url호출 시 S3 HeadObject를 먼저 수행하여 오브젝트 존재 여부를 확인하면, 클라이언트에게 404 대신 명확한 에러 메시지를 전달할 수 있다. 단, 성능 영향을 고려하여 캐싱 전략이 필요하다.
Monitoring#
- Singleshot agent의 S3 404 에러 모니터링:
service:cupixworks-capture-singleshot-agent status:error "statusCode\":404"
- API에서 download_single_resource 호출 후 S3 404 발생 패턴 추적:
service:cupixworks-capture-singleshot-agent "TransferManager::failTask"
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard
- 발생 빈도 1회로, 특정 capture(678511)의 pano 원본 이미지가 S3에 없는 데이터 상태 문제이다. 그러나 API에서 S3 오브젝트 존재 여부를 검증하지 않는 구조적 문제가 있어, 다른 capture에서도 동일 패턴이 발생할 수 있다.