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#
- 2025-08-23 15:37 KST — 최초 발생 (first_seen)
- 2026-07-24 11:09 KST — 최근 발생 (last_seen)
- 2026-08-04 — RCA 수행 (last_seen 이 14일 retention 밖이라 Datadog 로그 0건)
Error Log#
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::Parameters 는 Hash 가 아니며 to_i 메서드를 정의하지 않으므로 NoMethodError 가 raise 된다. 앞단의 방어 코드가 params[:key].blank? 만 검사(값 유무만 확인, 타입은 미검사)하기 때문에 nested 파라미터가 통과하고, to_i 호출 시점에 예외가 터진다. 이 예외는 ClientErrorController/ServerErrorController 의 rescue_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/nearest→Api::V1::PanosController#nearest - 방어 코드는 값의 존재만 검사하고 타입은 검사하지 않는다:
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 의.split도record_ids를 nested 로 보내면 유사한NoMethodError: undefined method 'split'을 낸다.) -
동일한
params[...].to_i패턴이 repo 전반에 존재하며, 가드가 없거나 값 유무만 검사하는 다수 지점이 잠재적 실패 지점이다:
@model.anchor_id = params[:anchor_id].to_i
source_user_id: params[:source_user_id].to_i,
target_user_id: params[:target_user_id].to_i,
- 예외가 500 으로 흐르는 이유 — rescue_from 체인에
NoMethodError가 없고RuntimeError매칭에도 걸리지 않는다:
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::Parameter 는 client_error_controller.rb:10,61-63 client_400_error 로 매핑되어 HTTP 400 을 반환한다. 즉 파싱 전에 타입을 검증해 이 예외를 raise 하면 별도의 상태코드 재매핑 없이도 malformed input 이 400 으로 귀결된다.
이 "파싱 전 타입 검증" 은 tesla 가 이미 채택한 관례다 — 동일 파라미터 계열에서 스칼라 타입을 강제하는 선례가 존재한다:
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) 로 거부한다. nearest 의 current_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 이 아니라 신뢰 가능하다.
사용한 쿼리:
service:cupixworks-api "undefined method `to_i' for an instance of ActionController::Parameters"
service:cupixworks-api "ActionController::Parameters" "to_i"
결과:
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::Parameters 에 to_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 | NoMethodError 를 rescue_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 nearest의current_pano_id/record_ids를.to_i/.split이전에 타입 검증한다. tesla 기존 관례(capture.rb:262)를 그대로 차용:current_pano_id—blank?검사(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::Parameter는client_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 추이 관찰:
service:cupixworks-api "ActionController::Parameters"
- API 서버 에러율 전반:
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::Parameters 를 Cupix::Errors::Parameter (400) 로 거부. 이 패턴은 tesla 기존 관례로 실재함 — app/concerns/parameter/capture.rb:262 raise Cupix::Errors::Parameter ... unless params[:warnings].is_a?(String). Cupix::Errors::Parameter → client_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 유지 + 파싱 검증). 즉시 조치를nearest의current_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|Exceptiongrep = 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등).