ES /docs

Excon::Error::NotFound: Expected(200) <=> Actual(404 Not Found) excon.error.response :body => "" :cookies => [ ] :header

RCA: Excon::Error::NotFound 404 on avatar HEAD

Overview#

What Happened#

cupixvista-excon APM 스팬에 Excon::Error::NotFound: Expected(200) <=> Actual(404 Not Found) 에러가 집계되었다. 이는 아바타를 설정하지 않은 사용자의 프로필을 렌더할 때 CarrierWave fog uploader 가 S3 객체 존재 여부를 확인하려고 HEAD .../avatar/users/{id}/original 요청을 보냈다가 404 를 받는 정상 경로다. fog-aws 가 이 404 를 곧바로 nil 로 흡수하므로 사용자 영향은 전혀 없으며, Datadog 의 excon 계측이 fog 의 rescue 이전에 HTTP 404 스팬을 error 로 태깅하면서 Error Tracking noise 로 표면화된 것이다. 16개월간 95건(<0.2/day)으로 매우 낮은 빈도다.

Quick Facts#

Field Value
exception.class Excon::Error::NotFound
exception.message Expected(200) <=> Actual(404 Not Found)
top_frame carrierwave-2.2.0/lib/carrierwave/storage/fog.rb:494
runtime Ruby 3.3.7 / carrierwave 2.2.0 / fog-aws 3.19.0
env production-us / dev (cupixvista deployment)

Affected Teams#

Team / Domain Error Count Impact
cupixvista-api (all tenants) 95 (16mo) 없음 — fog 가 404 를 nil 로 흡수, avatar_urls 는 정상적으로 nil 반환

Timeline#

  1. 2025-05-19 18:24 KST — first_seen. 아바타 미설정 user 130 프로필 렌더 시 S3 HEAD 404 (dev 버킷).
  2. 2026-07-31 14:26 KST — last_seen. user 9265 동일 패턴, error.handling: handled 스팬 5건 연속.
  3. 2026-08-05 — RCA 수행. status-board active 없음, verdict noise.

Error Log#

Datadog Logs

text
Expected(200) <=> Actual(404 Not Found)
excon.error.response
  :body              => ""
  :headers           => {
    "Content-Type"      => "application/xml"
    "Server"            => "AmazonS3"
  }
  :host              => "s3.us-west-2.amazonaws.com"
  :method            => "HEAD"
  :path              => "/cupixvista-tesla-dev-hosting-uswe2/5cecc2a9cff658228ebae4da9c74fc518370ba20/avatar/users/130/original"
  :status            => 404
  :status_line       => "HTTP/1.1 404 Not Found\r\n"

Impact#

  • Service: cupixvista-excon
  • 발생 횟수: 95
  • 최초 발생: 2025-05-19 18:24 KST
  • 최근 발생: 2026-07-31 14:26 KST

Root Cause Summary#

cupixvista-excon 은 실제 서비스가 아니라 tesla vista 배포의 Datadog excon APM 계측 스팬 이름이다 (config/initializers/datadog.rb:46, LAUNCH_MODE=CUPIXVISTAcupixvista-excon). 아바타를 설정하지 않은 사용자의 프로필을 조회할 때 Avatable#avatar_urls (avatable.rb:10) 및 #cleanup_avatar! (avatable.rb:28) 가 CarrierWave 의 avatar.file 을 접근하고, 이는 fog storage 에서 directory.files.head(path) (carrierwave/storage/fog.rb:494) = S3 HEAD 요청으로 변환된다. 해당 사용자는 S3 에 아바타 객체가 없으므로 S3 가 404 를 반환하고, fog-aws 는 이 404 를 rescue Excon::Errors::NotFound → nil (fog/aws/models/storage/files.rb:116-117) 로 완전히 흡수하여 exists?=false / file.nil?=true 로 정상 처리한다. 따라서 애플리케이션 로직상 결함이 아니며 사용자 영향은 0이다. 다만 Datadog 의 excon 계측이 fog 의 rescue 보다 먼저 실행되어 HTTP 404 스팬을 error 로 태깅하기 때문에 Error Tracking 에 noise 로 집계된다.

Technical Analysis#

Code Path#

  • Entry point: app/models/concerns/avatable.rb:10 (avatar_urls) 또는 :28 (cleanup_avatar!)
  • Uploader 정의: app/uploaders/avatar_uploader.rb:5 (storage :fog)
  • Failure point (스팬 태깅 지점): carrierwave-2.2.0/lib/carrierwave/storage/fog.rb:494 → S3 HEAD
  • Rescue point (흡수 지점): fog-aws-3.19.0/lib/fog/aws/models/storage/files.rb:116-117

아바타 URL 을 만들기 전에 avatar.file.nil? 로 파일 존재 여부를 확인한다. 아바타 미설정 user 는 이 접근에서 S3 HEAD 를 유발한다.

app/models/concerns/avatable.rb:9-18ruby
def avatar_urls
  if avatar.file.nil?
    nil
  else
    {
      thumb: "#{avatar.url(:thumb)}#{avatar.thumb.identifier == 'original' ? '' : "_#{avatar.identifier}"}?#{updated_at.to_i}",
      original: "#{avatar.url}?#{updated_at.to_i}"
    }
  end
end

avatar.file 접근은 fog storage 에서 S3 HEAD 로 이어진다. 객체가 없으면 여기서 404 스팬이 발생한다.

carrierwave-2.2.0/lib/carrierwave/storage/fog.rb:493-495ruby
def file
  @file ||= directory.files.head(path)
end

fog-aws 는 그 404 를 즉시 nil 로 흡수한다. 이 지점이 애플리케이션 관점에서 "파일이 없다" 를 나타내는 정상 신호다.

fog-aws-3.19.0/lib/fog/aws/models/storage/files.rb:108-118ruby
def head(key, options = {})
  requires :directory
  data = service.head_object(directory.key, key, options)
  normalize_headers(data)
  file_data = data.headers.merge({
    :key => key
  })
  new(file_data)
rescue Excon::Errors::NotFound
  nil
end

기대 동작: 아바타 미설정 user 는 avatar_urlsnil 을 반환해야 한다 (정상). 실제 동작: 정확히 그렇게 동작한다 — 404 는 fog 내부에서 nil 로 흡수되어 사용자에게 어떤 에러도 전파되지 않는다. 유일한 부작용은 fog 의 rescue 이전에 Datadog excon 계측(config/initializers/datadog.rb:46)이 HTTP 404 스팬을 error 로 태깅한다는 것뿐이다.

참고로 server_error_controller.rb:15Excon::Error::Socket 만 rescue 하고 NotFound 는 포함하지 않지만, fog 가 먼저 404 를 삼키므로 이 예외가 컨트롤러까지 도달하는 경로 자체가 없다.

app/controllers/concerns/server_error_controller.rb:15-16ruby
rescue_from Excon::Error::Socket,
            Seahorse::Client::NetworkingError, with: :badgateway_on_integrated_service_502_error

Log Evidence#

Datadog 로그 검색은 0건이다 — fog 의 rescue 로 인해 이 404 는 애플리케이션 로거로 전달되지 않으며, 오직 APM 스팬으로만 존재한다.

text
LOG "service:cupixvista-excon "Excon::Error::NotFound"" (now-14d): 0
LOG "service:cupixvista-api "avatar/users""            (now-14d): 0
LOG "service:cupixvista-excon "404 Not Found""          (now-14d): 0

실제 증거는 APM 스팬에서 확인된다. last_seen (2026-07-31T05:26:44Z) 부근 창을 조회했다.

text
service:cupixvista-excon status:error
window: 2026-07-31T04:00:00Z ~ 2026-07-31T07:00:00Z
text
spans: 5
byType: { "Excon::Error::NotFound": 5 }

스팬 샘플 (5건 모두 동일):

json
{
  "path": "/cupixvista-tesla-hosting-uswe2/6930ce4ba15c1825c98df15728751e7f1364e561/avatar/users/9265/original",
  "method": "HEAD",
  "error.handling": "handled",
  "base_service": "cupixvista-api",
  "error.message": "Expected(200) <=> Actual(404 Not Found)\nexcon.error.response\n  :body ..."
}

error.handling: handledbase_service: cupixvista-api, HEAD + avatar/users/{id}/original 경로가 위 코드 경로를 확증한다. last_seen 시각과 스팬 시각이 일치하며, representative(user 130) 대비 user id(9265)/hex 만 다를 뿐 root cause 는 동일하다 → representative 는 stale 이 아니다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 아바타 미설정 user 의 존재 확인용 S3 HEAD 404 를 fog 가 흡수하고, excon 계측이 그 이전에 스팬을 error 태깅해 noise 로 집계 스팬 100% Excon::Error::NotFound HEAD avatar/users/{id}/original, error.handling: handled; fog files.rb:116-117 rescue → nil; avatable.rb:10 avatar.file.nil? Confirmed
H2 실제 서비스 cupixvista-excon 에서 발생한 코드 결함 cupixvista-excon 은 datadog.rb:46 excon 계측 스팬 이름, 실제 앱은 tesla (vista 배포). base_service=cupixvista-api Rejected
H3 컨트롤러가 404 를 잘못 처리해 사용자에게 500/502 전파 server_error_controller.rb:15 는 Excon::Error::Socket 만 rescue fog 가 404 를 먼저 흡수 → 예외가 컨트롤러 도달 불가, 로그 0건, avatar_urls 정상 nil 반환 Rejected
H4 representative(user 130, dev 버킷)가 stale 이고 현재는 다른 에러 last_seen 부근 스팬이 동일 메시지·경로·클래스(user id/hex 만 변동) → NOT stale Rejected

Fix Recommendation#

즉시 조치 (Critical)#

코드 변경 불필요. 이는 애플리케이션 결함이 아니라 정상 경로에서 발생하는 handled 404 이며 사용자 영향이 0이다. Error Tracking 에서 이 이슈를 IGNORE 처리할 것을 권장한다.

단기 개선 (1주 이내)#

선택적: 아바타 컬럼이 DB 에 set 되어 있으나 S3 객체가 없는 고아 참조를 정리하면 HEAD 404 자체를 줄일 수 있다. 다만 fog 가 이미 안전하게 흡수하므로 우선순위는 낮다.

장기 개선 (재발 방지)#

error.handling: handled 이면서 4xx 인 excon 스팬은 애플리케이션 로직상 정상 신호이므로 Datadog Error Tracking 집계에서 제외하는 계측/필터 정책을 검토한다. 다만 필터를 이 두 조건(handled + 4xx)으로 정확히 정의하면 실제로 제외되는 대상은 좁으므로, 도입 전 아래 실측 근거로 범위를 확정한다.

14d 창(service:cupixworks-excon status:error + service:cupixvista-excon status:error) 의 excon error 스팬을 error.type × HTTP status × error.handling 로 교차 집계한 결과다.

text
cupixvista-excon (14d, 55 spans):
    55  Excon::Error::NotFound      | 404 | handled   ← HEAD avatar/users/{id}/original

cupixworks-excon (14d, 1351 spans):
   455  Excon::Error::MovedPermanently   | 301 | unset
   352  Excon::Error::Forbidden          | 403 | unset
   335  Excon::Error::NotFound           | 404 | handled   ← HEAD avatar/users/{id}/original
   145  Excon::Error::BadRequest         | 400 | unset
    61  Excon::Error::ServiceUnavailable | 503 | handled
     2  Excon::Error::InternalServerError| 500 | handled
     1  OpenSSL::SSL::SSLError           |  ?  | unset

필터(handled + 4xx)로 추가 제외 가능한 이슈 — 즉, 이번 avatar 404 와 동일 근거로 안전하게 제외되는 것:

  • cupixworks-excon Excon::Error::NotFound 404 (HEAD avatar/users/{id}/original) — 이번 클러스터의 cupixworks(비-vista) 배포 쌍둥이. base_service cupixworks-api, env production, path 100% avatar/users/{id}/original (예: .../avatar/users/53364/original), 335/335 error.handling: handled. fog 가 404 를 nil 로 흡수하는 동일 정상 경로 → 필터로 안전하게 제외 가능.

필터로 제외되지 않는 이슈 (근거상 제외 부적합) — 함께 묶어 제외하면 안 됨:

  • Excon::Error::Forbidden 403 (352건)error.handling: unset (handled 아님). 이는 thumbnail 업로드 PUT 의 S3 SignatureDoesNotMatch/InvalidAccessKeyId = 전역 fog credential stale/rotated 문제(0b3cfc31 계열, infra/ops 이슈). fog 가 삼키지 않는 실제 실패 신호이므로 4xx 라도 제외 대상 아님.
  • Excon::Error::BadRequest 400 (145건)error.handling: unset. thumbnail PUT 의 S3 AuthorizationHeaderMalformed (region us-east-1 vs us-west-2) = 전역 config/initializers/carrierwave.rb fog_credentials region 미지정 버그(ef652994 episode). 4xx 지만 실제 코드 결함이므로 제외하면 진짜 버그를 숨긴다.
  • Excon::Error::ServiceUnavailable 503 (61건)handled 이지만 5xx 이므로 "4xx" 조건에 미포함. thumbnail PUT 의 S3 SlowDown rate-limit(3f013e58 → 60e250e6/cluster 55dad51d 로 병합됨). 4xx 필터로는 잡히지 않으며, 5xx handled 까지 제외 범위를 넓히려면 별도 판단 필요.
  • Excon::Error::MovedPermanently 301 (455건) — 3xx, unset. 4xx 아님. 별도 검토 대상.

결론: error.handling: handled + 4xx 필터로 이번 avatar 404 와 함께 안전하게 추가 제외 가능한 것은 cupixworks-excon 의 avatar 404 (HEAD existence check) 하나다. 403/400 은 handling=unset(그리고 400 은 실제 버그), 503 은 5xx 라 이 필터에 포함되지 않으므로 blanket 제외 금지. 더 넓게 정리하려면 "존재 확인용 HEAD 요청 자체를 계측에서 제외"(method:HEAD + fog head 경로)를 별도 정책으로 검토하는 편이 정확하다.

Monitoring#

기존 발생 추이를 관찰하기 위한 timeseries 쿼리 (변화 없어야 정상):

text
sum:trace.excon.request.errors{service:cupixvista-excon,resource_name:excon.request}.as_count()

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (코드 변경 없음, ET IGNORE 권장)

Noise Verdict#

noise — 아바타 미설정 user 의 S3 존재 확인 HEAD 404 를 fog 가 nil 로 정상 흡수하는 handled 경로로, 사용자 영향이 없고 코드 결함이 아니다.

Revision History#

Revision 1#

Feedback: 장기 개선 방향(handled 4xx excon 스팬을 Error Tracking 집계에서 제외)을 적용할 때, 어떤 이슈들을 추가로 집계에서 제외할 수 있는지 확인 요청.

판정:

피드백 항목 판정 근거
장기 개선 필터로 추가 제외 가능한 이슈 확인 부분 수용 14d 실측 span 교차 집계 결과, error.handling: handled + 4xx 조건을 정확히 만족하는 것은 cupixworks-excon 의 avatar 404 (HEAD avatar/users/{id}/original, base_service cupixworks-api, 335/335 handled, path 100% avatar existence check) 하나뿐이다 — 이번 클러스터의 cupixworks 배포 쌍둥이로 동일 root cause. 나머지 excon sibling 은 필터 조건 미충족: Forbidden 403(352, handling=unset, credential 문제 0b3cfc31), BadRequest 400(145, handling=unset, region misconfig 실제 버그 ef652994), ServiceUnavailable 503(61, handled 지만 5xx — SlowDown 3f013e58→60e250e6/55dad51d), MovedPermanently 301(455, 3xx unset). 따라서 "추가로 제외 가능"은 avatar 404 로 한정되며, 403/400/503/301 을 함께 제외하면 실제 버그(400)를 숨기거나 의도와 다른 범위를 제외하게 됨.

변경 사항:

  • ## Fix Recommendation### 장기 개선 (재발 방지) 를 재작성. 기존의 "avatar 404, thumbnail 404 등 동일 계열" 이라는 모호한 표현을 실측 근거로 교체.
  • 14d excon error span 교차 집계 표(error.type × status × handling)를 추가.
  • 필터로 안전하게 제외 가능한 이슈(cupixworks-excon avatar 404)와 제외 부적합한 이슈(403/400/503/301)를 근거와 함께 명시적으로 분리.
  • blanket 제외 금지 및 대안 정책(HEAD existence-check 계측 제외) 제언 추가.

추가 조사 내용:

  • Datadog spans /api/v2/spans/events/searchservice:cupixworks-excon status:errorservice:cupixvista-excon status:error (now-14d) 조회 후 attributes.error.{type,handling} × attributes.http.status_code × attributes.http.url/path_group 교차 집계.
  • cupixworks-excon: 1351 spans — MovedPermanently 301(455)/Forbidden 403(352)/NotFound 404 handled(335)/BadRequest 400(145)/ServiceUnavailable 503(61)/InternalServerError 500(2)/SSLError(1).
  • cupixvista-excon: 55 spans — 100% NotFound 404 handled HEAD avatar.
  • cupixworks-excon 404 handled 335 건은 100% path avatar/users/{id}/original, base_service cupixworks-api, env production → 이번 cupixvista 클러스터와 동일 근거 확인.
  • 클러스터 파일 대조: errors/55dad51d-20fe-479d-819f-81f11b0dba9b.md (thumbnail S3 503 SlowDown, ET 60e250e6, merged_fingerprints: et:3f013e58) — 503 은 5xx 라 이번 4xx 필터 범위 밖임을 확인.