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_jwt 는 private_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#
- 2026-06-04 13:47 KST —
a6a535e8d(TSLA-13082):api_token→ 내부 user JWT (service_jwt) 인증으로 교체 - 2026-06-11 09:31 KST —
54f4417c2(TSLA-13082 rubocopLint/IneffectiveAccessModifier자동수정):private_class_method :service_jwt, :find_or_create_internal_user추가 →self.class.service_jwt호출이 깨짐 - 2026-07-10 10:31–10:40 KST — 클러스터 first_seen/last_seen (Representative "403 Forbidden", Datadog 14일 retention 밖이라 원본 로그 조회 불가)
- 2026-08-04 17:37–17:48 KST — dev 환경에서
NoMethodError private method 'service_jwt'100+건 폭증 (현재 실제 신호)
Error Log#
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_recipes가create_user_recipe를 호출
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)
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로 마킹됨
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::ExceptionWithResponse 는 NoMethodError 를 잡지 못하므로 subscriber (UserRecipeGenerator#created) 레벨까지 전파되어 processing 'facility_permission.created' failed: ... 로 로깅됨.
원인 도입 커밋:
end++ private_class_method :service_jwt, :find_or_create_internal_user end endLog Evidence#
사용한 Datadog 쿼리:
service:cupixworks-api status:error "Cupix::NotificationService"
가장 최근(2026-08-04) 실제 신호 — NoMethodError (Representative "403 Forbidden" 과 다름):
{
"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" }
}
{
"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 레벨로 산발적으로 남아 있으나:
{
"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_jwt 로 self.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를 제거하면 rubocopLint/IneffectiveAccessModifier가 재발할 수 있으므로, 호출부와 접근자를 함께 정리해야 한다.
- (권장) 클래스 내부에서 private class method 를 호출할 때는 explicit receiver 없이 호출 가능하다.
- 근거: Ruby 에서 private method 는
self.class.service_jwt같은 explicit receiver 로 호출 불가 → NoMethodError. 접근자 변경 커밋(54f4417c2)이 기존 호출부(a6a535e8d)를 깨뜨림.
단기 개선 (1주 이내)#
create_user_recipe/find_email_recipe/subscribe/unsubscribe의 인증 방식 일관성 확인:create_user_recipe만service_jwt(Bearer) 로 전환되었고 나머지는 여전히@user.api_token(x-cupix-auth) 을 사용 중. 의도된 부분 마이그레이션인지 검증하고,subscribe경로가 계속@user.api_token을 쓴다면 만료/무효 토큰 403 은 여전히 발생함.subscribe/unsubscribe의rescue 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 용):
service:cupixworks-api status:error "private method `service_jwt'"
subscribe 403 (별도 변종) 추적:
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 생성이 실패한다.