ES /docs

TransferManager::downloadAppProcessingOptions | {"name":"HTTPError","host":"api-tesla.cupix.internal

RCA: TransferManager::downloadAppProcessingOptions | HTTPError 403 Forbidden

Error Log#

Datadog Logs

text
TransferManager::downloadAppProcessingOptions | {"name":"HTTPError","host":"api-tesla.cupix.internal","hostname":"api-tesla.cupix.internal","method":"GET","path":"/api/v1/captures/681313/resources/processing_options/download","protocol":"http:","url":"http://api-tesla.cupix.internal/api/v1/captures/681313/resources/processing_options/download","statusCode":403,"statusMessage":"Forbidden","headers":{...}}

Impact#

  • Service: cupixworks-capture-preprocessor-agent
  • Team: hodulee
  • 발생 횟수: 1 (이 클러스터), 5+ (최근 14일간 동일 패턴)
  • 최초 발생: 2026-04-15T02:29:45.344Z
  • 최근 발생: 2026-04-15T02:29:45.344Z

Root Cause Summary#

capture-preprocessor-agent가 capture 681313의 processing_options 리소스를 다운로드하려 할 때, 해당 capture에 processing_options 리소스가 DB에 존재하지 않아 tesla API가 Cupix::Errors::NotFound를 발생시켰다. 그런데 tesla의 client_error_controller.rb에서 NotFound 예외를 보안상의 이유로 HTTP 403 Forbidden으로 매핑하고 있어, agent 측에서는 권한 오류인지 리소스 부재인지 구분할 수 없다. agent의 TransferManager::downloadAppProcessingOptions는 이 에러를 catch 후 false를 반환하고, PreprocessorService는 다운로드 실패를 무시한 채 나머지 전처리 파이프라인을 계속 진행한다. 즉, 이 에러는 처리 실패를 유발하지 않는 non-blocking 에러이지만, error 레벨로 로깅되어 불필요한 알림 노이즈를 생성하고 있다.

Technical Analysis#

Code Path#

  1. Entry point: preprocessor-service.ts:93run() 메서드가 capture preprocessing 시작
  2. Download 호출: preprocessor-service.ts:120await this.downloadAppProcessingOptions(cpCapture) 호출
typescript
// preprocessor-service.ts:120
await this.downloadAppProcessingOptions(cpCapture);
await this.downloadPanoFiles(cpCapture);  // 다운로드 실패와 관계없이 계속 진행
  1. Wrapper 메서드: preprocessor-service.ts:384-395 — capture 타입이 App인 경우에만 다운로드 시도
typescript
// preprocessor-service.ts:384-395
private downloadAppProcessingOptions = async (cpCapture: CPCapture): Promise<void> => {
    logger.debug('PreprocessorService::downloadAppProcessingOptions | begin');
    if (!cpCapture.isTypeApp) {
        logger.debug('PreprocessorService::downloadAppProcessingOptions | end - capture type is not app ');
        return;
    }
    const isDone = await this.transferManager.downloadAppProcessingOptions(cpCapture);
    if (isDone) await cpCapture.loadProcessingOptions();
    logger.debug('PreprocessorService::downloadAppProcessingOptions | end');
};
  1. Failure point: transfer.manager.ts:231-249 — HTTP 403 에러 발생 시 catch하여 error 로그 출력 후 false 반환
typescript
// transfer.manager.ts:231-249
downloadAppProcessingOptions = async (cpCapture: CPCapture): Promise<boolean> => {
    const path = cpCapture.appProcessingOptionsFilePath;
    const url = cpCapture.appProcessingOptionsDownloadUrl;
    const accessToken = this.cupixAuth.accessToken;
    if (path == undefined || url == undefined) {
        return false;
    }
    try {
        fs.writeFileSync(path, await download(url, undefined, {
            headers: { 'Content-Type':'application/json', 'X-CUPIX-AUTH': accessToken },
            retries: 10
        } as any));
        return true;
    } catch (error) {
        logger.error('TransferManager::downloadAppProcessingOptions | %s', JSON.stringify(error));
        return false;  // 에러를 삼키고 false 반환 — 처리는 계속됨
    }
};
  1. API 측 에러 원인: multiple_resourcable_controller.rb:145-151processing_options 리소스가 DB에 없으면 NotFound 발생
ruby
# multiple_resourcable_controller.rb:145-151
def set_multiple_resource
  kind_or_key = ApplicationRecord.sanitize_sql(params[:kind_or_key])
  @resource = @model.resources.where(kind: kind_or_key).or(@model.resources.where(key: kind_or_key)).first
  raise Cupix::Errors::NotFound.new(code: 'ARG10002', reason: 'Resource not found') and return if @resource.nil?
end
  1. 403 매핑: client_error_controller.rb:27,55-57NotFound를 403으로 매핑하는 보안 패턴
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

기대 동작: processing_options 리소스가 없는 capture에 대해서는 다운로드를 시도하지 않거나, warn 레벨로 로깅 실제 동작: 리소스 존재 여부와 관계없이 무조건 다운로드를 시도하고, 실패 시 error 레벨로 로깅

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-capture-preprocessor-agent status:error "TransferManager::downloadAppProcessingOptions"
Time range: 2026-04-01T00:00:00Z to 2026-04-15T12:00:00Z

14일간 최소 5건의 동일 패턴 에러가 확인됨 (captures: 681313, 40566, 40567, 40565, 71063):

json
{
  "timestamp": "2026-04-15 11:29:45 KST",
  "status": "error",
  "message": "TransferManager::downloadAppProcessingOptions | {\"name\":\"HTTPError\",\"statusCode\":403,\"statusMessage\":\"Forbidden\",\"path\":\"/api/v1/captures/681313/resources/processing_options/download\"}"
}

API 측 로그 — 동일 요청에 대한 tesla API 응답:

text
service:cupixworks-api 681313 "processing_options"
Time range: 2026-04-15T01:00:00Z to 2026-04-15T04:00:00Z
json
{
  "timestamp": "2026-04-15 11:29:47 KST",
  "status": "info",
  "message": "[403] GET /api/v1/captures/681313/resources/processing_options/download (Api::V1::CapturesController#download_multiple_resource)",
  "error": {
    "reason": "Resource not found",
    "code": "ARG10002",
    "message": "Resource not found",
    "class": "Cupix::Errors::NotFound"
  }
}

API 로그에서 실제 에러 클래스가 Cupix::Errors::NotFound임을 확인 — 즉, 권한 문제가 아니라 리소스가 DB에 존재하지 않는 것이 원인이다.

처리 계속 진행 확인 — 에러 발생 후 나머지 파이프라인이 정상 진행됨:

json
{"timestamp": "2026-04-15 11:29:45 KST", "status": "error", "message": "TransferManager::downloadAppProcessingOptions | ...403..."}
{"timestamp": "2026-04-15 11:29:51 KST", "status": "info", "message": "PreprocessorService::setVideoImageMatchData | begin - capture_id: 681313"}
{"timestamp": "2026-04-15 11:30:49 KST", "status": "info", "message": "PreprocessorService::runPreprocessor | photo count: 0, video count: 1, app AR file path: /tmp/workspace/681313/app/processing_options.json"}
{"timestamp": "2026-04-15 11:30:53 KST", "status": "info", "message": "BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/681313"}

processing_options 리소스 부재는 광범위 패턴 — 단순 show 요청에서도 동일 에러 빈발:

text
service:cupixworks-api "processing_options" "Resource not found"

4월 15일 하루에만 다수의 capture(681320, 68677, 68676, 68675, 1714-1719 등)에서 동일한 Resource not found 응답 확인.

Fix Recommendation#

즉시 조치 (Critical)#

agent 측 로그 레벨 변경transfer.manager.ts:246

현재 logger.error로 기록되지만, 이 에러는 처리를 중단시키지 않는 expected condition이다. HTTP 403 (실제로는 리소스 부재)인 경우 logger.warn으로 변경하여 error 노이즈를 줄여야 한다. 추가로 statusCode를 로그 메시지에 명시적으로 포함하면 디버깅이 용이해진다.

단기 개선 (1주 이내)#

리소스 존재 여부 사전 확인preprocessor-service.ts:384-395

downloadAppProcessingOptions 호출 전에 capture의 리소스 목록을 조회하여 processing_options가 존재하는지 확인하는 로직을 추가하거나, loadCaptureResources(line 108)에서 이미 로드한 리소스 목록에 processing_options가 포함되어 있는지 체크한 후 다운로드를 시도해야 한다. 이렇게 하면 불필요한 HTTP 요청과 에러 로그를 원천 차단할 수 있다.

장기 개선 (재발 방지)#

  1. API 응답 코드 개선client_error_controller.rb:27,55-57에서 NotFound를 403으로 매핑하는 보안 패턴은 외부 클라이언트에는 적절하지만, 내부 서비스 간 통신에서는 에러 원인 판단을 어렵게 만든다. 내부 API 호출 시에는 실제 에러 코드(ARG10002)와 reason(Resource not found)을 응답 body에 포함하고 있으므로, agent 측에서 이 body를 파싱하여 리소스 부재와 권한 오류를 구분할 수 있도록 개선할 수 있다.

  2. retries: 10 설정 재검토transfer.manager.ts:242에서 리소스가 존재하지 않는 경우에도 10회 재시도를 수행한다. 403/404 등 클라이언트 에러에 대해서는 재시도하지 않도록 retry 정책을 개선해야 한다.

Monitoring#

  • error 로그 노이즈 감소 확인:
text
service:cupixworks-capture-preprocessor-agent status:error "TransferManager::downloadAppProcessingOptions"
  • 로그 레벨 변경 후 warn 레벨 모니터링:
text
service:cupixworks-capture-preprocessor-agent status:warn "TransferManager::downloadAppProcessingOptions"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial — 이 에러는 처리 파이프라인을 중단시키지 않으며, processing_options 리소스가 없는 capture에서 expected condition으로 발생한다. 로그 레벨 변경만으로 즉시 노이즈를 줄일 수 있다.