ES /docs

NoMethodError: undefined method `to_i' for an instance of ActionController::Parameters

RCA: NoMethodError: undefined method `to_i' for an instance of ActionController::Parameters

Overview#

What Happened#

cupixworks-api (tesla) 의 어떤 컨트롤러/factory 경로에서 스칼라 값을 기대하고 params[:key].to_i 를 호출했으나, 클라이언트가 해당 파라미터를 nested object/array 형태로 전송해 Rails 가 이를 중첩 ActionController::Parameters 로 파싱했다. ActionController::Parameters 에는 to_i 가 없어 NoMethodError 가 발생했고, 이 예외는 rescue_from 체인에 걸리지 않아 HTTP 500 으로 노출되었다. 2025-08-23 ~ 2026-07-24 사이 26건 발생한 저빈도 이슈다 (약 11개월간 < 3건/월).

Quick Facts#

Field Value
exception.class NoMethodError
exception.message undefined method \to_i' for an instance of ActionController::Parameters`
top_frame 확인 불가 (representative 에 stack frame 없음)
env production
service cupixworks-api (repo: tesla)

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (tesla) 26 잘못된 형태의 파라미터를 보낸 소수 요청이 400 대신 500 응답 수신

Timeline#

  1. 2025-08-23 15:37 KST — 최초 발생 (first_seen)
  2. 2026-07-24 11:09 KST — 최근 발생 (last_seen)
  3. 2026-08-04 — RCA 수행 (last_seen 이 14일 retention 밖이라 Datadog 로그 0건)

Error Log#

Datadog Logs

text
undefined method `to_i' for an instance of ActionController::Parameters

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 26
  • 최초 발생: 2025-08-23 15:37 KST
  • 최근 발생: 2026-07-24 11:09 KST

Root Cause Summary#

tesla 컨트롤러/factory 여러 지점이 params[:key].to_i 형태로 스칼라 정수 변환을 수행한다. 클라이언트가 current_pano_id[x]=y 처럼 파라미터를 nested 형태로 보내면 Rails 의 strong parameters 는 이를 중첩 ActionController::Parameters 인스턴스로 파싱한다. ActionController::ParametersHash 가 아니며 to_i 메서드를 정의하지 않으므로 NoMethodError 가 raise 된다. 앞단의 방어 코드가 params[:key].blank? 만 검사(값 유무만 확인, 타입은 미검사)하기 때문에 nested 파라미터가 통과하고, to_i 호출 시점에 예외가 터진다. 이 예외는 ClientErrorController/ServerErrorControllerrescue_from 목록에 없고, tesla 에 rescue_from StandardError 폴백도 존재하지 않으므로 (server_error_controller.rb, client_error_controller.rb 확인), 어떤 handler 에도 매칭되지 않아 Rails 기본 처리로 HTTP 500 이 반환된다.

NoMethodError 가 500 으로 흐르는 것 자체는 올바른 동작이다 — 예외가 발생한 시점(params[:current_pano_id].to_i)에서 이미 NoMethodError 는 프로그래밍 에러(타입이 맞지 않는 객체에 메서드 호출)이므로, 이를 400 으로 재매핑하면 진짜 nil 참조/오브젝트 버그까지 4xx 로 숨겨질 위험이 있다. 따라서 근본 해결은 상태코드 재매핑이 아니라, .to_i 를 호출하기 전에 파라미터가 기대하는 스칼라 타입인지 검증(parse)하여 잘못된 형태의 입력을 애초에 Cupix::Errors::Parameter (400) 로 거부하는 것이다. 이렇게 하면 malformed input 은 .to_i 에 도달하지 않아 NoMethodError 자체가 발생하지 않고, NoMethodError 는 실제 버그를 드러내는 500 신호로 온전히 남는다.

Technical Analysis#

Code Path#

  • Entry point 예시: GET /api/v1/reviews/:review_key/panos/nearestApi::V1::PanosController#nearest
  • 방어 코드는 값의 존재만 검사하고 타입은 검사하지 않는다:
app/controllers/api/v1/panos_controller.rb:102-119ruby
def nearest
  raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'record_ids is required') if params[:record_ids].blank?
  raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'current_pano_id is required') if params[:current_pano_id].blank?

  review_repository_instance = ReviewRepository.new(current_user: current_user)
  review_repository_instance.show(params[:review_key])

  record_ids = params[:record_ids].split(',').map(&:to_i)
  # ...
  panos = PanoRepository.nearest_by_records(
    current_pano_id: params[:current_pano_id].to_i,   # nested Parameters → NoMethodError
    record_ids: accessible_record_ids
  )
  • Failure point: app/controllers/api/v1/panos_controller.rb:117 — 클라이언트가 current_pano_id 를 nested 형태(current_pano_id[a]=1)로 보내면 params[:current_pano_id] 는 중첩 ActionController::Parameters 가 된다. blank? 는 false 이므로 line 104 의 가드를 통과하고, line 117 의 .to_i 에서 NoMethodError 발생. (동일 경로 line 109 의 .splitrecord_ids 를 nested 로 보내면 유사한 NoMethodError: undefined method 'split' 을 낸다.)

  • 동일한 params[...].to_i 패턴이 repo 전반에 존재하며, 가드가 없거나 값 유무만 검사하는 다수 지점이 잠재적 실패 지점이다:

app/concerns/parameter/anchorable.rb:25ruby
@model.anchor_id = params[:anchor_id].to_i
app/controllers/api/v1/admin/teams_controller.rb:72-73ruby
source_user_id: params[:source_user_id].to_i,
target_user_id: params[:target_user_id].to_i,
  • 예외가 500 으로 흐르는 이유 — rescue_from 체인에 NoMethodError 가 없고 RuntimeError 매칭에도 걸리지 않는다:
app/controllers/concerns/server_error_controller.rb:18-20ruby
rescue_from ActiveRecord::LockWaitTimeout,
            Errno::ENOMEM,
            RuntimeError, with: :badgateway_on_system_502_error

기대 동작: 잘못된 파라미터 타입은 .to_i 호출 이전에 검증되어 400 (Cupix::Errors::Parameter) 으로 거부되어야 하며, 검증을 통과한 경우에만 정상 파싱된다. 실제 동작: 타입 검증이 없어 nested ActionController::Parameters.to_i 까지 도달하고 NoMethodError 가 unhandled 로 빠져나가 500 Internal Server Error 로 응답된다 (NoMethodError < NameError < StandardError, RuntimeError 아님 → line 20 매칭 안 됨, ClientErrorController 목록에도 없음).

Cupix::Errors::Parameterclient_error_controller.rb:10,61-63 client_400_error 로 매핑되어 HTTP 400 을 반환한다. 즉 파싱 전에 타입을 검증해 이 예외를 raise 하면 별도의 상태코드 재매핑 없이도 malformed input 이 400 으로 귀결된다.

이 "파싱 전 타입 검증" 은 tesla 가 이미 채택한 관례다 — 동일 파라미터 계열에서 스칼라 타입을 강제하는 선례가 존재한다:

app/concerns/parameter/capture.rb:260-266ruby
def set_warning(params)
  if params[:warnings].present?
    raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: 'warnings must be comma separated string') unless params[:warnings].is_a?(String)

    @model.add_warning(params[:warnings])
  end
end

params[:warnings] 가 String 이 아니면(즉 nested Parameters/Array 이면) 처리 전에 Cupix::Errors::Parameter (400) 로 거부한다. nearestcurrent_pano_id/record_ids 도 동일 패턴으로 .to_i/.split 이전에 타입을 검증하면 된다.

Log Evidence#

last_seen(2026-07-24 11:09 KST)이 14일 Datadog log retention 밖이라 로그 검색은 0건이다. representative message 는 고정된 Ruby 예외 상수(method 명 to_i 와 receiver class 명만 다름)이므로 stale 이 아니라 신뢰 가능하다.

사용한 쿼리:

text
service:cupixworks-api "undefined method `to_i' for an instance of ActionController::Parameters"
text
service:cupixworks-api "ActionController::Parameters" "to_i"

결과:

text
Found 0 logs

(Error Tracking 은 14일 log retention 을 넘겨 occurrence 를 집계 보관하므로, ET issue 에 26건이 남아 있어도 원본 로그는 조회되지 않는다.)

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 클라이언트가 정수 파라미터를 nested object/array 로 전송 → Rails 가 ActionController::Parameters 로 파싱 → .to_i 호출 시 NoMethodError ActionController::Parametersto_i 미정의; params[:key].to_i 패턴 다수 존재(panos_controller.rb:117, anchorable.rb:25, admin/teams_controller.rb:72 등); 방어 코드가 blank? 만 검사(panos_controller.rb:104) Confirmed
H2 서버 내부 로직이 params 자리에 잘못된 객체를 주입해 발생 메시지가 ActionController::Parameters 인스턴스를 명시 → 요청 파라미터 파싱 결과물임. 내부 조립 객체라면 다른 class 로 표기됨 Rejected
H3 특정 배포로 인한 regression 없음 11개월에 걸쳐 균일 저빈도(26건)로 발생 → 특정 배포 상관 없음 Rejected
H4 예외가 이미 4xx 로 정상 처리되고 있다 없음 NoMethodError 는 rescue_from 목록에 없고 RuntimeError 계열도 아님(server_error_controller.rb:18-20), rescue_from StandardError 폴백도 없음 → unhandled 500 Rejected
H5 NoMethodErrorrescue_from 으로 400 에 매핑해야 한다 없음 NoMethodError 는 프로그래밍 에러 신호이므로 400 으로 재매핑하면 진짜 nil/객체 버그까지 4xx 로 숨겨짐. 500 유지가 올바르며, 해결은 .to_i 이전 타입 검증(parse)으로 malformed input 이 예외를 만들지 않도록 하는 것 (capture.rb:262 선례) Rejected

Fix Recommendation#

방침 (Revision #2 반영)#

  • NoMethodError 는 500 server error 로 유지한다. rescue_from NoMethodError → 400 같은 상태코드 재매핑은 채택하지 않는다 — 그렇게 하면 진짜 nil 참조/객체 버그까지 4xx 로 숨겨진다. 대신 예외가 발생하기 전에 입력을 제대로 파싱(타입 검증)해서, malformed input 이 .to_i/.split 에 도달하지 않게 한다.

즉시 조치 (Critical)#

  • panos_controller.rb:102-119 nearestcurrent_pano_id/record_ids.to_i/.split 이전에 타입 검증한다. tesla 기존 관례(capture.rb:262)를 그대로 차용:
    • current_pano_idblank? 검사(line 104) 통과 후 .to_i(line 117) 이전에 is_a?(String) (또는 정수 문자열 여부)을 검사하고, 아니면 raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'current_pano_id must be an integer').
    • record_ids.split(line 109) 이전에 is_a?(String) 검사, 아니면 동일하게 Cupix::Errors::Parameter (400) raise.
  • Cupix::Errors::Parameterclient_error_controller.rb:10,61-63 를 통해 이미 400 으로 매핑되므로 별도 handler 변경이 불필요하다.

단기 개선 (1주 이내)#

  • 산재한 raw params[...].to_i 호출(예: anchorable.rb:25, admin/teams_controller.rb:72-73)에 대해 동일한 "파싱 전 타입 검증" 패턴을 적용하거나, 스칼라를 강제하는 공통 파싱 헬퍼를 도입한다. 헬퍼는 ActionController::Parameters/Array 를 받으면 Cupix::Errors::Parameter (400) 를 raise 하고, 유효한 정수 문자열만 Integer(...) 로 파싱한다. NoMethodError 를 rescue 하는 방식이 아니라 애초에 발생시키지 않는 방식이라는 점이 핵심이다.

장기 개선 (재발 방지)#

  • API 파라미터 계약을 스키마(예: dry-validation / strong-typed contract)로 선언해 컨트롤러 진입 시점에 타입 위반을 400 으로 일괄 거부(=올바른 파싱을 진입점에 강제). 개별 .to_i 산발 방어를 제거. NoMethodError 는 이 계층을 통과한 뒤 발생하는 진짜 프로그래밍 에러 신호로서 500 을 유지한다.

Monitoring#

  • 파라미터 타입 위반으로 인한 미분류 500 추이 관찰:
text
service:cupixworks-api "ActionController::Parameters"
  • API 서버 에러율 전반:
text
service:cupixworks-api status:error

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard (개별 가드는 trivial, 공통 헬퍼/스키마화는 standard)
  • 사용자 영향: 정상 클라이언트에는 영향 없음. 잘못된 형태의 요청을 보내는 소수 클라이언트/스캐너만 500 대신 400 을 받게 됨.

Noise Verdict#

noise — 스칼라를 기대하는 파라미터에 nested 값을 보낸 malformed client input 에 의한 예외로, 서버 로직 결함이 아니라 입력 검증(파싱) 누락이 원인인 저빈도 이슈다. 해결 방향은 상태코드 재매핑이 아니라 .to_i 이전 타입 검증(parse)이며, NoMethodError 자체는 진짜 버그 신호로서 500 을 유지한다.

Revision History#

Revision 2#

Feedback: "noMethodError 는 500 server error 로 처리하고, 이번 케이스는 parse 를 제대로 하는 방향으로 다시 조사"

판정:

피드백 항목 판정 근거
NoMethodError 는 500 server error 로 처리 (400 재매핑하지 말 것) 수용 tesla 에 NoMethodError handler 및 rescue_from StandardError 폴백이 모두 없어(server_error_controller.rb:1-66, client_error_controller.rb:1-92) Rails 기본 처리로 500 이 반환됨 — 현재 동작이 이미 500 이며 올바르다. 기존 RCA 의 "저비용 대안: rescue_from NoMethodError → 400" 권장을 제거하고, 400 재매핑은 진짜 nil/객체 버그를 숨긴다는 근거로 H5(Rejected)를 추가함.
이번 케이스는 parse 를 제대로 하는 방향으로 재조사 수용 근본 해결 = .to_i(panos_controller.rb:117)/.split(:109) 호출 이전에 파라미터 타입을 검증(parse)하여 nested ActionController::ParametersCupix::Errors::Parameter (400) 로 거부. 이 패턴은 tesla 기존 관례로 실재함 — app/concerns/parameter/capture.rb:262 raise Cupix::Errors::Parameter ... unless params[:warnings].is_a?(String). Cupix::Errors::Parameterclient_error_controller.rb:10,61-63 client_400_error = 400 확인. 타입 검증을 통과한 입력만 파싱되므로 malformed input 은 NoMethodError 를 만들지 않음.

변경 사항:

  • Root Cause Summary: NoMethodError 의 500 유지가 올바른 동작임을 명시하고, 해결 방향을 상태코드 재매핑 → ".to_i 이전 타입 검증(parse)" 으로 재정의.
  • Technical Analysis > Code Path: Cupix::Errors::Parameter → 400 매핑 경로(client_error_controller.rb:10,61-63)와 tesla 기존 파싱-전-검증 선례(capture.rb:260-266) 코드 스니펫 추가.
  • Fix Recommendation: rescue_from NoMethodError 대안을 삭제. "방침" 항목 신설(500 유지 + 파싱 검증). 즉시 조치를 nearestcurrent_pano_id/record_ids 타입 검증으로 구체화(capture.rb:262 차용).
  • Hypotheses: H5(400 재매핑 제안) Rejected 추가. H4 에 rescue_from StandardError 폴백 부재 근거 보강.
  • Noise Verdict: 상태코드 매핑 정비 → 파싱 검증으로 문구 수정.

추가 조사 내용:

  • tesla develop 브랜치 실코드 재확인 (panos_controller.rb:102-126, server_error_controller.rb, client_error_controller.rb, error_base_controller.rb).
  • rescue_from StandardError|Exception grep = 0 hits → NoMethodError 는 어떤 tesla handler 에도 안 걸림(Rails 기본 500).
  • 리포지토리 전반 is_a?(String)/is_a?(ActionController::Parameters) grep 으로 "파싱 전 타입 검증" 관례가 광범위함을 확인(capture.rb:262, bim_revision.rb:87,95, admin/tesla_configs_controller.rb:9 등).