ES /docs

RestClient::Forbidden: 403 Forbidden

RCA: NoMethodError private method service_jwt (Cupix::NotificationService)

Overview#

What Happened#

cupixworks-api (tesla) 의 Cupix::NotificationService#create_user_recipe 가 notification-service 호출 시 인증 헤더를 만들기 위해 self.class.service_jwt 를 호출하는데, service_jwtprivate_class_method 로 선언되어 있어 explicit receiver (self.class) 로 호출할 수 없다. 그 결과 NoMethodError: private method 'service_jwt' called for class Cupix::NotificationService 가 발생한다. FacilityPermission 생성/권한 부여 이벤트마다 pub/sub subscriber UserRecipeGenerator 가 이 경로를 타므로, 2026-08-04 dev 환경에서 약 11분 동안 100건 이상 폭증했다.

이 클러스터의 Representative Error 는 RestClient::Forbidden: 403 Forbidden 이지만, 이는 Error Tracking 이 여러 변종을 하나의 issue 로 묶으면서 고정한 STALE 샘플이다. 최근(last_seen 인근 및 그 이후) 로그의 실제 신호는 위의 service_jwt NoMethodError 이므로 이를 root cause 로 분석한다 (아래 Log Evidence 의 discrepancy 항목 참조).

Quick Facts#

Field Value
exception.class NoMethodError
exception.message private method 'service_jwt' called for class Cupix::NotificationService
top_frame lib/cupix/notification_service.rb:20 (self.class.service_jwt)
runtime Ruby / Rails (tesla), Sidekiq/pub-sub subscriber
deploy origin/develop (buggy). 54f4417c2 이후. origin/master 는 미영향
env development (dev cluster, s3.me-central2 / cupixworks-dev-hosting-*)

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (notification recipe 생성) 100+ (2026-08-04 17:37–17:48 burst, 그 외 산발) FacilityPermission 부여 시 user email recipe (record_preview_ready / record_processing_completed / facility_new_project) 미생성 → 해당 사용자 이메일 알림 누락

Timeline#

  1. 2026-06-04 13:47 KSTa6a535e8d (TSLA-13082): api_token → 내부 user JWT (service_jwt) 인증으로 교체
  2. 2026-06-11 09:31 KST54f4417c2 (TSLA-13082 rubocop Lint/IneffectiveAccessModifier 자동수정): private_class_method :service_jwt, :find_or_create_internal_user 추가 → self.class.service_jwt 호출이 깨짐
  3. 2026-07-10 10:31–10:40 KST — 클러스터 first_seen/last_seen (Representative "403 Forbidden", Datadog 14일 retention 밖이라 원본 로그 조회 불가)
  4. 2026-08-04 17:37–17:48 KST — dev 환경에서 NoMethodError private method 'service_jwt' 100+건 폭증 (현재 실제 신호)

Error Log#

Datadog Logs

text
403 Forbidden

(위는 클러스터의 Representative Error. 실제 최근 신호는 Log Evidence 참조.)

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 463 (Error Tracking 누적, 여러 변종 포함)
  • 최초 발생: 2026-07-10 10:31 KST
  • 최근 발생: 2026-07-10 10:40 KST (클러스터 기준. 실제 최근 로그는 2026-08-04 17:48 KST)

Root Cause Summary#

TSLA-13082 에서 notification-service 인증을 opaque api_token 에서 내부 user JWT 로 교체하면서 Cupix::NotificationService.service_jwt 클래스 메서드를 추가했고, create_user_recipe 는 이를 Authorization: "Bearer #{self.class.service_jwt}" 로 호출한다. 이후 rubocop Lint/IneffectiveAccessModifier 자동수정 커밋(54f4417c2)이 private_class_method :service_jwt, :find_or_create_internal_user 를 추가했다. Ruby 에서 private method 는 explicit receiver 로 호출할 수 없으며 self.class 조차 explicit receiver 이므로, self.class.service_jwt 호출이 NoMethodError: private method 'service_jwt' called for class Cupix::NotificationService 로 실패한다. FacilityPermission 생성 시 pub/sub subscriber UserRecipeGenerator 가 매번 create_user_recipe 를 호출하므로 권한 부여마다 예외가 발생한다. 이 버그는 origin/develop 에만 존재하고 origin/master(production)에는 없다 (master 는 아직 @user.api_token 사용).

Technical Analysis#

Code Path#

  • Entry point: pub/sub 이벤트 facility_permission.created / facility_permission.full_permission_enabled
  • UserRecipeGenerator#created_create_recipes_for_facility_permission_create_recipescreate_user_recipe 를 호출
lib/cupix/pub_sub/subscribers/user_recipe_generator.rb:34-42ruby
def _create_recipes(opts = {})
  %w[
    record_preview_ready
    record_processing_completed
    facility_new_project
  ].each do |recipe_name|
    Cupix::NotificationService.new(user: opts[:user]).create_user_recipe(recipe_name, team_id: opts[:team_id], facility_key: opts[:facility_key])
  end
end
  • Failure point: create_user_recipe 가 인증 헤더 생성 시 self.class.service_jwt 호출 (origin/develop)
lib/cupix/notification_service.rb:15-22 (origin/develop)ruby
response = Cupix::HttpClient.post(
  "#{@service_url}/api/recipes/v2",
  params.to_json,
  {
    content_type: :json,
    Authorization: "Bearer #{self.class.service_jwt}"
  }
)
  • service_jwt 는 클래스 메서드로 정의된 뒤 private_class_method 로 마킹됨
lib/cupix/notification_service.rb:213-243 (origin/develop)ruby
def self.service_jwt
  if @service_jwt && @service_jwt_exp && @service_jwt_exp > 30.minutes.from_now.to_i
    return @service_jwt
  end
  # ...
  @service_jwt = Cupix::Auth::AccessToken.encode(session: session, expires_in: 24.hours.to_i)
  @service_jwt_exp = 24.hours.from_now.to_i
  @service_jwt
end
# ...
private_class_method :service_jwt, :find_or_create_internal_user

기대 동작: self.class.service_jwt 가 캐시된 내부 JWT 를 반환하여 Authorization: Bearer <jwt> 헤더를 구성. 실제 동작: service_jwt 가 private class method 이므로 explicit receiver self.class 로 호출 불가 → NoMethodError: private method 'service_jwt' called for class Cupix::NotificationService 즉시 발생. rescue RestClient::ExceptionWithResponseNoMethodError 를 잡지 못하므로 subscriber (UserRecipeGenerator#created) 레벨까지 전파되어 processing 'facility_permission.created' failed: ... 로 로깅됨.

원인 도입 커밋:

lib/cupix/notification_service.rb (54f4417c2, rubocop Lint/IneffectiveAccessModifier)
     end++    private_class_method :service_jwt, :find_or_create_internal_user   end end

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api status:error "Cupix::NotificationService"

가장 최근(2026-08-04) 실제 신호 — NoMethodError (Representative "403 Forbidden" 과 다름):

json
{
  "timestamp": "2026-08-04 17:48:58",
  "status": "error",
  "message": "processing 'facility_permission.created' failed: private method `service_jwt' called for class Cupix::NotificationService",
  "class": "Cupix::PubSub::Subscribers::UserRecipeGenerator",
  "function": "created",
  "error": { "msg": "private method `service_jwt' called for class Cupix::NotificationService" }
}
json
{
  "timestamp": "2026-08-04 17:48:58",
  "status": "error",
  "message": "processing 'facility_permission.full_permission_enabled' failed: private method `service_jwt' called for class Cupix::NotificationService",
  "class": "Cupix::PubSub::Subscribers::UserRecipeGenerator",
  "function": "full_permission_enabled",
  "error": { "msg": "private method `service_jwt' called for class Cupix::NotificationService" }
}

볼륨: service:cupixworks-api status:error "Cupix::NotificationService" 쿼리가 2026-08-04 17:37:51 ~ 17:48:58 구간에서 limit 100 을 가득 채움 (약 11분 100+건 폭증).

Discrepancy note (stale representative): 클러스터의 Representative Error 는 403 Forbidden 이며, 이는 Cupix::NotificationService#subscribe (구 코드, @user.api_token 사용) 가 notification-service authorizer 로부터 403 을 받아 Cupix::Logger.error(e.response) 로 로깅하던 별개 변종이다. 실제로 아래처럼 최근 window 에도 subscribe 403 이 error 레벨로 산발적으로 남아 있으나:

json
{
  "timestamp": "2026-08-01 03:46:03",
  "status": "error",
  "message": "Forbidden",
  "class": "Cupix::NotificationService",
  "function": "subscribe"
}

클러스터의 first_seen/last_seen(2026-07-10)은 Datadog 14일 retention 밖이라 원본 403 로그는 조회되지 않았다(쿼리 service:cupixworks-api status:error "RestClient::Forbidden" → 0건). 반면 최근(last_seen 이후) window 의 지배적 신호는 service_jwt NoMethodError 이므로 이를 root cause 로 판단한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 private_class_method :service_jwtself.class.service_jwt 호출이 깨져 NoMethodError 발생 notification_service.rb:20 self.class.service_jwt + :243 private_class_method; Datadog 최근 로그 private method 'service_jwt' called for class 100+건; 도입 커밋 54f4417c2 Confirmed
H2 Representative 403 Forbidden — notification-service authorizer 가 @user.api_token 을 거부 (client auth 실패, noise) authorizer.ts:31-32 non-200 → 403; subscribe 403 error 로그 (7/22–8/1) 이는 STALE representative 변종. 최근 지배 신호는 NoMethodError 이고, subscribe 경로의 401/403 자체는 별도 개선 대상. 클러스터의 current 신호가 아님 Rejected (as current root cause)
H3 Thumbnail/S3 InvalidAccessKeyId 403 (Excon) 가 원인 7/22 [Thumbnail][Facility] Excon 403 로그 존재 완전히 다른 코드 경로(CarrierWave/S3), class/message 상이. 이 issue 와 무관 Rejected
H4 production(master) 도 영향 dev 로그에서 관측 origin/master:notification_service.rb@user.api_token 사용, service_jwt 미존재; 버그는 origin/develop 에만 병합됨 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 대상 파일: lib/cupix/notification_service.rb (origin/develop)
  • create_user_recipe (line 20) 의 인증 헤더 생성이 service_jwt 를 호출하는 방식과 service_jwt 의 접근 지정자가 서로 모순됨. 둘 중 하나로 정합화해야 함:
    • (권장) 클래스 내부에서 private class method 를 호출할 때는 explicit receiver 없이 호출 가능하다. create_user_recipe 는 인스턴스 메서드이므로 self.class.service_jwt 대신 클래스 컨텍스트로 위임하는 방식(예: private class method 를 감싸는 접근자)을 쓰거나, service_jwt 를 public class method 로 되돌린다. 단순히 private_class_method 를 제거하면 rubocop Lint/IneffectiveAccessModifier 가 재발할 수 있으므로, 호출부와 접근자를 함께 정리해야 한다.
  • 근거: Ruby 에서 private method 는 self.class.service_jwt 같은 explicit receiver 로 호출 불가 → NoMethodError. 접근자 변경 커밋(54f4417c2)이 기존 호출부(a6a535e8d)를 깨뜨림.

단기 개선 (1주 이내)#

  • create_user_recipe / find_email_recipe / subscribe / unsubscribe 의 인증 방식 일관성 확인: create_user_recipeservice_jwt(Bearer) 로 전환되었고 나머지는 여전히 @user.api_token(x-cupix-auth) 을 사용 중. 의도된 부분 마이그레이션인지 검증하고, subscribe 경로가 계속 @user.api_token 을 쓴다면 만료/무효 토큰 403 은 여전히 발생함.
  • subscribe/unsubscriberescue RestClient::ExceptionWithResponse 에서 401/403(client-auth)과 5xx(server) 를 분리해 401/403 은 warn 로 낮추어 알람 노이즈 축소 (별도 개선, 코드 버그 아님).

장기 개선 (재발 방지)#

  • private/public 접근자 변경 시 호출부 회귀를 잡는 spec 추가: Cupix::NotificationService.new(user: user).create_user_recipe(...) 가 실제로 notification-service 호출까지 진행하는지 검증 (현재 spec 이 이 경로를 커버하지 않아 rubocop 자동수정 회귀가 CI 를 통과함).
  • rubocop 자동수정(--autocorrect)이 런타임 호출부를 깨뜨릴 수 있는 access-modifier 변경은 리뷰 필수 항목으로 지정.

Monitoring#

배포 후 재발 여부 확인 쿼리 (release dashboard timeseries widget 용):

text
service:cupixworks-api status:error "private method `service_jwt'"

subscribe 403 (별도 변종) 추적:

text
service:cupixworks-api status:error @class:Cupix::NotificationService @function:subscribe

Risk Assessment#

  • Risk level: medium (dev 환경 한정, production 미영향이나 FacilityPermission 부여 시 email recipe 생성 100% 실패)
  • 예상 복잡도: trivial (접근자/호출부 1~2줄 정합화)

Noise Verdict#

bug — rubocop 자동수정이 추가한 private_class_method :service_jwt 때문에 self.class.service_jwt 호출이 NoMethodError 로 깨진 명백한 코드 결함으로, FacilityPermission 부여마다 email recipe 생성이 실패한다.