Excon::Error::BadRequest: Expected(200) <=> Actual(400 Bad Request) excon.error.response :body => "<?xml version=\"1.0\"
RCA: Excon::Error::BadRequest — S3 AuthorizationHeaderMalformed (region mismatch)
Overview#
What Happened#
Api::V1::CapturesController#update 에서 thumbnails= 파라미터를 처리할 때 CarrierWave 가 S3 로 썸네일을 업로드하는데, 이 PUT 요청이 us-east-1 리전으로 SigV4 서명되어 us-west-2 에 있는 cupix-tesla-static 버킷이 400 AuthorizationHeaderMalformed 로 거부한다. 최근 (2026-08-03 ~ 08-04) 로그·스팬에서도 동일하게 재현되며, us-west-2 production 에서 30개 이상의 서로 다른 capture 에 대해 발생하는 systematic 한 설정 결함이다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Excon::Error::BadRequest |
| exception.message | Expected(200) <=> Actual(400 Bad Request) / S3 AuthorizationHeaderMalformed (region 'us-east-1' is wrong; expecting 'us-west-2') |
| top_frame | carrierwave/storage/fog.rb:118 cache! → app/concerns/parameter.rb:21 |
| runtime | Ruby 3.3.0, Rails 7.2.2, carrierwave 2.2.0, fog-aws 3.19.0, excon 0.100.0 |
| env | production, region us-west-2, tenant cupix |
| service (APM) | cupixworks-excon (Datadog excon instrumentation span, 실제 앱은 tesla / cupixworks-api) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupixworks-api (tesla) — Capture 썸네일 업로드 | now-14d 기준 155 error spans (30+ distinct capture) | PATCH /api/v1/captures/{id} 에 thumbnails 를 포함할 때 썸네일 S3 캐시 저장 실패 |
Timeline#
- 2024-09-30 16:08 KST — 최초 발생 (first_seen).
- 2026-07-22 ~ 2026-08-04 KST — retention window 내 지속 발생 (일 5~35건),
us-west-2production 다수 tenant capture. - 2026-08-04 08:00 KST — 최근 발생 (last_seen). Representative Error 와 동일 메시지·경로 재현 확인.
Error Log#
Expected(200) <=> Actual(400 Bad Request)
excon.error.response
:body => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Error><Code>AuthorizationHeaderMalformed</Code><Message>The authorization header is malformed; the region 'us-east-1' is wrong; expecting 'us-west-2'</Message><Region>us-west-2</Region><RequestId>QHDYCA9QS4XD93E2</RequestId><HostId>XH4H06fOx7maJygS6VSWjLf1cDAKSs4pwGpyDAj/lmPIz5KQN016ewzYJg9Pu5pnRqx1+5GeqaA=</HostId></Error>"
:host => "cupix-tesla-static.s3.amazonaws.com"
:method => "PUT"
:path => "/cupix-tesla-static/cupix-tesla-static/uploads/tmp/1727680104-47100118228020-0001-8152/2xd8g87mivct_thumbnail.jpg"
:port => 443
:status => 400
:status_line => "HTTP/1.1 400 Bad Request\r\n"
Impact#
- Service:
cupixworks-api(APM spancupixworks-excon, 실제 repo = tesla) - 발생 횟수: 79 (cluster) / retention window 155 error spans
- 최초 발생: 2024-09-30 16:08 KST
- 최근 발생: 2026-08-04 08:00 KST
Root Cause Summary#
config/initializers/carrierwave.rb 의 전역 CarrierWave.configure config.fog_credentials 에 region 이 지정되어 있지 않다. fog-aws 는 region 이 없으면 기본값 us-east-1 을 사용하고 SigV4 서명에 이 리전을 포함한다. CarrierWave 가 thumbnails= 대입 시점에 임시 캐시 디렉터리(uploads/tmp/...)로 파일을 저장하는 cache! 단계에서, path_style: true + region 미지정인 전역 fog_credentials 로 연결을 만들어 cupix-tesla-static.s3.amazonaws.com (지역 없는 글로벌 엔드포인트) 로 PUT 하며 us-east-1 로 서명한다. 실제 버킷은 us-west-2 이므로 S3 가 AuthorizationHeaderMalformed (region 'us-east-1' is wrong; expecting 'us-west-2') 로 400 을 반환한다. ThumbnailUploader#initialize 가 per-instance 로 올바른 리전(us-west-2)/endpoint 를 merge 하지만, 캐시 store 경로가 전역 설정을 사용하는 것이 문제의 핵심이다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/api/v1/captures_controller.rb:47(Api::V1::CapturesController#update) app/repositories/capture_repository.rb:182-194(CaptureRepository#update) →set_parameters(params)호출
def update(params = {})
super
set_parameters(params)
begin
@model.save!
rescue StandardError => e
raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: 'Invalid argument', message: e.message)
end
@model
end
app/concerns/parameter.rb:20-22—thumbnails파라미터를 모델에 대입. 이 대입이 CarrierWave 의mount_uploaders :thumbnailscache 단계를 트리거한다. (참고: 이 경로는 단수:thumbnail경로와 달리set_thumbnail_version을 호출하지 않는다.)
if params[:thumbnails].present? && @model.respond_to?(:thumbnails)
@model.thumbnails = params[:thumbnails]
end
- Failure point: 전역 CarrierWave 설정에
region이 없음.
CarrierWave.configure do |config|
config.fog_credentials = {
provider: 'AWS',
aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'] || Rails.application.credentials[Rails.env.to_sym][:aws][:access_key_id],
aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] || Rails.application.credentials[Rails.env.to_sym][:aws][:secret_access_key],
path_style: true
# region 없음 → fog-aws 가 us-east-1 로 서명
}
config.fog_directory = $AWS[:s3][:static_bucket_name]
end
- per-instance 설정은 올바른 리전/endpoint 를 갖고 있으나(대조군), 실패 스팬의 host 는 지역 없는
cupix-tesla-static.s3.amazonaws.com이고 서명은us-east-1이므로 캐시 store 연결이 전역 설정을 사용했음을 보여준다.
def v2_settings
{
asset_host: "https://#{$AWS[:cloudfront][:static_hostname]}",
fog_credentials: {
region: $AWS[:s3][:static_bucket_region], # production = us-west-2
endpoint: v2_s3_endpoint # https://s3.us-west-2.amazonaws.com
},
fog_directory: $AWS[:s3][:static_bucket_name], # cupix-tesla-static
thumbnail_path: "#{s3_path_hex_v2}/#{signed_content_prefix}thumbnail/#{self.class.to_s.underscore.pluralize}/#{id}"
}
end
기대 동작: 썸네일 캐시/저장 PUT 이 us-west-2 로 서명되어 cupix-tesla-static 버킷에 성공. 실제 동작: 캐시 store 연결이 전역 fog_credentials(region 미지정)를 사용해 us-east-1 로 서명 → S3 가 400 AuthorizationHeaderMalformed 반환.
Failure 스택 (excon → fog-aws → carrierwave cache!):
excon/middlewares/expects.rb:13 (Excon::Error::BadRequest)
fog-aws-3.19.0/lib/fog/aws/requests/storage/put_object.rb:48 put_object
carrierwave-2.2.0/lib/carrierwave/storage/fog.rb:344 store
carrierwave-2.2.0/lib/carrierwave/storage/fog.rb:118 cache!
carrierwave-2.2.0/lib/carrierwave/mounter.rb:176 handle_error
carrierwave-2.2.0/lib/carrierwave/mount.rb:309 thumbnails=
app/concerns/parameter.rb:21 set_parameters
app/concerns/parameter/capture.rb:68 set_parameters
app/repositories/capture_repository.rb:185 update
app/controllers/api/v1/captures_controller.rb:47 update
Log Evidence#
cupixworks-excon 은 Datadog excon 계측 스팬(APM)이며 이 에러는 로그로 남지 않는다 (log 검색 0건). 실제 발생은 error span 으로 확인된다.
로그 검색 (0건 — 이 클러스터의 excon 에러는 로그가 아니라 APM span):
service:cupixworks-api "AuthorizationHeaderMalformed"
service:cupixworks-excon "AuthorizationHeaderMalformed"
Span 검색 (실제 증거):
service:cupixworks-excon status:error
now-14d 기준 error span 분류 (error.type):
{
"Excon::Error::MovedPermanently": 485,
"Excon::Error::Forbidden": 365,
"Excon::Error::NotFound": 340,
"Excon::Error::BadRequest": 160,
"Excon::Error::ServiceUnavailable": 57,
"Excon::Error::InternalServerError": 1
}
AuthorizationHeaderMalformed 를 담은 스팬은 155건, 모두 region: us-west-2. 최근 스팬(2026-08-03T23:00:24Z, last_seen 과 일치)의 핵심 필드:
timestamp: 2026-08-03T23:00:24.497Z region: us-west-2 tenant: cupix
error.type: Excon::Error::BadRequest
wrong: us-east-1 expecting: us-west-2 method: PUT
host: cupix-tesla-static.s3.amazonaws.com
path: /cupix-tesla-static/cupix-tesla-static/uploads/tmp/1785798013-283253285979069-0173-5138/whitingturner.747652_1484242_pointcloud_thumbnail_3.jpg
app frame: app/concerns/parameter.rb:21 set_parameters
발생 분포 (파일 suffix / 일별):
{
"suffix": { "#_#_pointcloud_thumbnail_#.jpg": 150, "thumb_...pointcloud_thumbnail_#.jpg": 5 },
"per_day": { "2026-08-03": 5, "2026-07-31": 15, "2026-07-30": 20, "2026-07-28": 15, "2026-07-27": 15, "2026-07-24": 20, "2026-07-23": 35, "2026-07-22": 20 },
"distinct_captures": 30
}
동일 path 가 초 단위로 5회 반복되는 패턴은 fog-aws idempotent middleware 의 재시도(fog/aws/storage.rb:703 rescue in _request)이며, 사용자 재시도가 아니라 단일 요청의 내부 retry 이다. 30개 이상의 서로 다른 capture(whitingturner, accoes, wgyates, tpc, gilbaneco, bblinc, clark-vdc ...)에 걸쳐 발생 = 특정 데이터가 아닌 광범위한 설정 결함.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 전역 CarrierWave.config.fog_credentials 에 region 미지정 → 캐시 store PUT 이 us-east-1 로 서명되어 us-west-2 버킷이 거부 |
carrierwave.rb:3-8 region 없음 + path_style: true; 실패 host 가 지역 없는 cupix-tesla-static.s3.amazonaws.com; 서명 리전 us-east-1; 스택이 storage/fog.rb:118 cache! (캐시 경로) |
없음 | Confirmed |
| H2 | Representative Error 가 stale (오래된 first_seen 샘플) | first_seen 2024-09-30 | 최근 스팬(2026-08-03 last_seen)이 동일 메시지·경로·리전으로 재현 | Rejected |
| H3 | S3 일시 장애 / region 라우팅 이슈 (transient) | S3 400 응답 | 메시지가 고정 상수(region 'us-east-1' is wrong)이고 22개월 지속·다수 tenant·항상 us-east-1 서명 → 결정론적 misconfig |
Rejected |
| H4 | per-instance ThumbnailUploader/v2_settings 가 region 을 잘못 넣음 |
thumbnails= 진입 |
v2_settings.fog_credentials.region = $AWS[:s3][:static_bucket_region](us-west-2), endpoint = https://s3.us-west-2.amazonaws.com 로 올바름 → 실패는 전역 캐시 연결에서 발생 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
config/initializers/carrierwave.rb의config.fog_credentials에 리전을 명시한다.config.fog_directory가$AWS[:s3][:static_bucket_name]인 것과 짝을 이루도록region: $AWS[:s3][:static_bucket_region](그리고 필요 시endpoint: "https://s3.#{$AWS[:s3][:static_bucket_region]}.amazonaws.com")을 추가한다. 이렇게 하면 CarrierWave 의 캐시(uploads/tmp/...) 및 store 연결이 올바른 리전으로 서명된다.- 근거: 실패는 per-instance 설정이 아니라 전역 캐시 연결에서 발생하며, 전역 fog_credentials 에 region 만 채우면 재현 경로가 해소된다.
단기 개선 (1주 이내)#
mount_uploaders :thumbnails대입 시set_thumbnail_version이 호출되지 않는 점(parameter.rb:20-22)을 검토한다. 단수:thumbnail경로는set_thumbnail_version을 호출하지만 복수 경로는 하지 않아thumbnail_version이 sys 값에 의존한다. 다만 실패 host/버킷은 v2 의cupix-tesla-static과 일치하므로 이 항목은 root cause 가 아니라 일관성 정리 대상이다 (uncertain — 수정 시 회귀 테스트 필요).- CarrierWave 캐시/store 실패를 사용자 응답(500)으로 전파하기 전에
set_parameters밖에서 명시적으로 감지·로깅하도록 정리한다. 현재CaptureRepository#update의begin/rescue는save!만 감싸고set_parameters(=캐시 시점)는 감싸지 않아 예외 매핑이 불명확하다.
장기 개선 (재발 방지)#
- S3 리전/버킷 설정을 단일 소스(
$AWS[:s3])에서 파생하도록 통일하고, 리전 미지정 시 부팅 시점에 실패하도록 검증(fail-fast)을 추가한다. - 스테이징에서
PATCH /api/v1/captures/{id}+thumbnails업로드에 대한 통합 테스트를 추가해 리전 서명 회귀를 조기 감지한다.
Monitoring#
- excon S3 400(AuthorizationHeaderMalformed) 스팬 추이를 추적한다. 배포 후 0 으로 수렴해야 한다.
service:cupixworks-excon status:error resource_name:PUT @error.type:Excon::Error::BadRequest
- capture 썸네일 업로드 경로의 에러율 추이:
service:cupixworks-api resource_name:"PATCH /api/v1/captures/{id}" status:error
Risk Assessment#
- Risk level: medium (설정 한 줄 추가지만 전역 CarrierWave 캐시/store 경로 전체에 영향)
- 예상 복잡도: trivial (region 키 추가) + 회귀 검증 standard
Noise Verdict#
bug — 전역 CarrierWave fog_credentials 에 region 이 빠져 캐시 업로드가 us-east-1 로 서명되어 us-west-2 버킷에서 결정론적으로 실패하는 실제 설정 결함으로, capture 썸네일 업로드가 지속적으로 깨진다.