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#
- 2025-05-19 18:24 KST — first_seen. 아바타 미설정 user 130 프로필 렌더 시 S3 HEAD 404 (dev 버킷).
- 2026-07-31 14:26 KST — last_seen. user 9265 동일 패턴,
error.handling: handled스팬 5건 연속. - 2026-08-05 — RCA 수행. status-board active 없음, verdict noise.
Error Log#
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=CUPIXVISTA → cupixvista-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 를 유발한다.
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 스팬이 발생한다.
def file
@file ||= directory.files.head(path)
end
fog-aws 는 그 404 를 즉시 nil 로 흡수한다. 이 지점이 애플리케이션 관점에서 "파일이 없다" 를 나타내는 정상 신호다.
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_urls 가 nil 을 반환해야 한다 (정상). 실제 동작: 정확히 그렇게 동작한다 — 404 는 fog 내부에서 nil 로 흡수되어 사용자에게 어떤 에러도 전파되지 않는다. 유일한 부작용은 fog 의 rescue 이전에 Datadog excon 계측(config/initializers/datadog.rb:46)이 HTTP 404 스팬을 error 로 태깅한다는 것뿐이다.
참고로 server_error_controller.rb:15 는 Excon::Error::Socket 만 rescue 하고 NotFound 는 포함하지 않지만, fog 가 먼저 404 를 삼키므로 이 예외가 컨트롤러까지 도달하는 경로 자체가 없다.
rescue_from Excon::Error::Socket,
Seahorse::Client::NetworkingError, with: :badgateway_on_integrated_service_502_error
Log Evidence#
Datadog 로그 검색은 0건이다 — fog 의 rescue 로 인해 이 404 는 애플리케이션 로거로 전달되지 않으며, 오직 APM 스팬으로만 존재한다.
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) 부근 창을 조회했다.
service:cupixvista-excon status:error
window: 2026-07-31T04:00:00Z ~ 2026-07-31T07:00:00Z
spans: 5
byType: { "Excon::Error::NotFound": 5 }
스팬 샘플 (5건 모두 동일):
{
"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: handled 와 base_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 로 교차 집계한 결과다.
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::NotFound404 (HEADavatar/users/{id}/original) — 이번 클러스터의 cupixworks(비-vista) 배포 쌍둥이. base_servicecupixworks-api, env production, path 100%avatar/users/{id}/original(예:.../avatar/users/53364/original), 335/335error.handling: handled. fog 가 404 를nil로 흡수하는 동일 정상 경로 → 필터로 안전하게 제외 가능.
필터로 제외되지 않는 이슈 (근거상 제외 부적합) — 함께 묶어 제외하면 안 됨:
Excon::Error::Forbidden403 (352건) —error.handling: unset(handled 아님). 이는 thumbnail 업로드 PUT 의 S3SignatureDoesNotMatch/InvalidAccessKeyId= 전역 fog credential stale/rotated 문제(0b3cfc31 계열, infra/ops 이슈). fog 가 삼키지 않는 실제 실패 신호이므로 4xx 라도 제외 대상 아님.Excon::Error::BadRequest400 (145건) —error.handling: unset. thumbnail PUT 의 S3AuthorizationHeaderMalformed(region us-east-1 vs us-west-2) = 전역config/initializers/carrierwave.rbfog_credentialsregion 미지정 버그(ef652994 episode). 4xx 지만 실제 코드 결함이므로 제외하면 진짜 버그를 숨긴다.Excon::Error::ServiceUnavailable503 (61건) —handled이지만 5xx 이므로 "4xx" 조건에 미포함. thumbnail PUT 의 S3SlowDownrate-limit(3f013e58 → 60e250e6/cluster 55dad51d 로 병합됨). 4xx 필터로는 잡히지 않으며, 5xx handled 까지 제외 범위를 넓히려면 별도 판단 필요.Excon::Error::MovedPermanently301 (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 쿼리 (변화 없어야 정상):
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/search로service:cupixworks-excon status:error및service: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_servicecupixworks-api, env production → 이번 cupixvista 클러스터와 동일 근거 확인. - 클러스터 파일 대조:
errors/55dad51d-20fe-479d-819f-81f11b0dba9b.md(thumbnail S3 503 SlowDown, ET60e250e6,merged_fingerprints: et:3f013e58) — 503 은 5xx 라 이번 4xx 필터 범위 밖임을 확인.