ES /docs

Elasticsearch::Transport::Transport::Errors::BadRequest: [400] {"error":{"root_cause":[{"type":"parsing_exception","reas

RCA: Elasticsearch multi_match [400] unknown token [START_OBJECT] after [query]

Overview#

What Happened#

cupixworks-api (tesla) 의 검색 엔드포인트(GET /api/v1/reviews, GET /api/v1/admin/quotes)에서 클라이언트가 q 파라미터를 scalar 문자열이 아니라 중첩 object (q[s]=..., q[number]=...) 로 보냈다. tesla 는 params[:q] 를 그대로 Elasticsearch multi_match 쿼리의 query 필드에 넣어, ES 가 [multi_match] unknown token [START_OBJECT] after [query] parsing_exception (HTTP 400) 을 반환했고, 이것이 tesla 에서 HTTP 502 로 표면화됐다. 14일 창 7건 전부 내부 테스트 트래픽(curl/Python-urllib, dev/qa 환경)이며 프로덕션/실사용자 영향은 관측되지 않았다.

Quick Facts#

Field Value
exception.class Elasticsearch::Transport::Transport::Errors::BadRequest
exception.message [400] ... parsing_exception "[multi_match] unknown token [START_OBJECT] after [query]" ... "[bool] failed to parse field [must]"
top_frame lib/cupix/query_option/base.rb:157-165 (set_search_string)
runtime Ruby on Rails (tesla), Elasticsearch (elasticsearch-model)
deploy origin/master @ 5d437c17 (Merged PR 89570, 2026-08-06)
env dev, qa (us-west-2)

Affected Teams#

Team / Domain Error Count Impact
cupix (dev) 4 GET /api/v1/reviews 502 (내부 테스트, curl/8.7.1)
admin (qa) 3 GET /api/v1/admin/quotes 502 (내부 테스트, Python-urllib/curl)

프로덕션 tenant 발생 없음. 전부 내부 테스트 클라이언트.

Timeline#

  1. 2026-07-24 16:26 KST — 최초 발생: GET /api/v1/admin/quotes (qa), q[number_eq]=... 중첩 object 입력 (col:60)
  2. 2026-08-03 20:47 ~ 21:20 KSTGET /api/v1/reviews (dev), q[s]=published_at desc 중첩 object 입력 버스트 (col:666)
  3. 2026-08-07 00:14 KST — 최근 발생 (cluster last_seen)
  4. 2026-08-07 — RCA 분석 수행

Error Log#

Datadog Logs

text
[400] {"error":{"root_cause":[{"type":"parsing_exception","reason":"[multi_match] unknown token [START_OBJECT] after [query]","line":1,"col":60}],"type":"x_content_parse_exception","reason":"[1:60] [bool] failed to parse field [must]","caused_by":{"type":"parsing_exception","reason":"[multi_match] unknown token [START_OBJECT] after [query]","line":1,"col":60}},"status":400}

Impact#

  • Service: cupixworks-api (실제 앱: tesla)
  • 발생 횟수: 10 (ET 집계) / 14d 로그 실측 7건
  • 최초 발생: 2026-07-24 16:26 KST
  • 최근 발생: 2026-08-07 00:14 KST

Root Cause Summary#

클라이언트가 검색 q 파라미터를 scalar 문자열 대신 중첩 object 로 전송한 것(?q[s]=published_at desc, ?q[number]=HNZ0G2MAY)이 근본 원인이다. Rails strong-params 는 이를 ActionController::Parameters object 로 파싱한다. SearchableController#get_query_optionquery_string: params[:q] 로 값을 타입 검증 없이 그대로 전달하고 (app/controllers/concerns/searchable_controller.rb:23), Cupix::QueryOption::Base#set_search_string 는 이 object 를 그대로 Elasticsearch multi_matchquery 필드에 삽입한다 (lib/cupix/query_option/base.rb:159). ES 는 query 다음에 scalar 를 기대하는데 object(START_OBJECT)를 만나 parsing_exception (HTTP 400) 을 던진다. tesla 코드 자체의 결함은 없으며, 유일한 문제는 이 malformed client input 이 4xx 가 아니라 HTTP 502 로 매핑된다는 status-code 오분류다. 발생은 전부 내부 dev/qa 테스트 트래픽이므로 verdict 는 noise 다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/reviews_controller.rb:14 (ReviewsController#index) / app/controllers/api/v1/admin/quotes_controller.rb:13 (QuotesController#index)
  • 두 컨트롤러 모두 get_query_option 을 호출해 params[:q]query_string 으로 전달:
app/controllers/concerns/searchable_controller.rb:18-27ruby
def get_query_option(enable_current_team: true)
  _query_option = QueryOption.new(
    per_page: params[:per_page],
    page: params[:page],
    sort: order_by(params[:sort], params[:order_by], params[:sort_order_by]),
    query_string: params[:q],   # 타입 검증 없이 그대로 전달
    visibility: params[:visibility],
    current_team: enable_current_team ? @current_team : nil,
    ids: params[:ids]
  )
  • @query_string.present? 가드(base.rb:94)는 값 존재만 확인 — 비어있지 않은 Hash/Parameters object 도 통과한다.
  • Failure point: lib/cupix/query_option/base.rb:157-165 — object 를 multi_match.query 에 삽입:
lib/cupix/query_option/base.rb:157-165ruby
@query[:bool][:must] << {
  multi_match: {
    query: @query_string,          # @query_string 이 object → ES 가 START_OBJECT 거부
    fields: search_fields,
    type: query_type,
    lenient: true,
    analyzer: self.class.current_class&.analyzer_exist?(analyzer: 'searchquery_analyzer') ? 'searchquery_analyzer' : 'standard'
  }
}
  • 실제 ES 호출은 ReviewRepository#_search::Review.search(...).paginate(...) (app/repositories/review_repository.rb:546-551) 가 반환하는 lazy response 를 열거할 때 발생한다. BaseRepository#search_searchbegin 블록 에서 호출하지만(base_repository.rb:71), lazy 실행이 어느 시점에 터지느냐에 따라 매핑이 달라진다.
app/repositories/base_repository.rb:70-98ruby
def search(query_option = nil)
  _search(query_option)                          # lazy — 아직 ES 미실행

  begin
    ...
    contents = ...(self.response.records)...      # 여기서 열거되면 :83 이 잡아 ARG13000(400)
  rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
    _reason = ...
    raise Cupix::Errors::Argument.new(code: 'ARG13000', reason: _reason)
  rescue StandardError => e
    raise Cupix::Errors::BadGateway.new(code: 'BG10002', reason: 'Bad Gateway error on Elasticsearch')
  end
  • 기대 동작: malformed q → 4xx client error. 실제 동작: 관측 응답은 HTTP 502 (server_error_controllerrescue_from Elasticsearch::Transport::Transport::Error → badgateway_on_elasticsearch_502_error, BG10002 로 매핑되는 경로) — lazy ES 예외가 base_repository begin 블록 밖에서 표면화됨. 이는 sibling ARG13000 No mapping found ... in order to sort on (eb0195be episode) 과 동일한 매핑 계열이다.

Log Evidence#

Datadog query (재현):

text
service:cupixworks-api "unknown token [START_OBJECT] after"

reviews 발생의 request 로그 — q 가 중첩 object 임을 확증:

json
{
  "message": "[502] GET /api/v1/reviews (Api::V1::ReviewsController#index)",
  "status": "info",
  "http": { "status_code": 502, "method": "GET", "url_details": { "path": "/api/v1/reviews" } },
  "params": { "q": { "s": "published_at desc" }, "per_page": "40", "fields": ["key","name","published_at","facility"] },
  "environment": "dev",
  "user_agent": "curl/8.7.1",
  "team": { "domain": "cupix", "id": 1 },
  "error": {
    "class": "Elasticsearch::Transport::Transport::Errors::BadRequest",
    "message": "[400] ...parsing_exception \"[multi_match] unknown token [START_OBJECT] after [query]\"... col:666"
  }
}

quotes 발생의 request 로그 — 동일 메커니즘, 다른 endpoint:

json
{
  "message": "[502] GET /api/v1/admin/quotes (Api::V1::Admin::QuotesController#index)",
  "params": { "per_page": "3", "q": { "number_eq": "HNZ0G2MAY" }, "fields": ["id","number"] },
  "environment": "qa",
  "user_agent": "Python-urllib/3.12"
}

분포 (14d, 7건):

text
environment: dev 4 / qa 3
path: /api/v1/reviews 4 / /api/v1/admin/quotes 3
user_agent: curl/8.7.1 4, Python-urllib/3.12 2, curl/8.17.0 1
domain: cupix 4 / admin 3

전부 내부 테스트 클라이언트(curl, Python-urllib), dev/qa 환경, 프로덕션 tenant 없음. col:60 (representative) 은 짧은 quotes 쿼리, col:666 은 긴 reviews 쿼리 — 같은 root cause, 쿼리 길이 차이일 뿐이다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 클라이언트가 q 를 중첩 object(q[s]=, q[number]=)로 보내 multi_match.query 에 object 삽입 → ES parsing_exception request 로그 params.q = {"s":"published_at desc"} / {"number_eq":"HNZ0G2MAY"}; searchable_controller.rb:23 query_string: params[:q] 타입 미검증; base.rb:159 query: @query_string Confirmed
H2 Elasticsearch 클러스터 장애/가용성 문제 (status-board dep:elasticsearch 최근 인시던트) status-board 에 최근 ES outage 3건 (08-04, 08-06) 이 cluster 는 그 인시던트 cluster_ids 에 미포함; 에러가 [400] parsing_exception = 쿼리 문법 오류이지 timeout/connection 아님; ES 는 정상 응답(400)함 Rejected
H3 tesla 코드가 잘못된 multi_match 쿼리를 구성하는 버그 set_search_string@query_string 을 그대로 사용 scalar q 입력 시 정상 동작; object 는 클라이언트 malformation, 코드는 계약대로 동작 Rejected
H4 Representative(col:60) 가 stale — 실제 최근 발생은 다른 메시지 ET 가 여러 col 변형을 한 이슈로 묶음 last_seen 창(08-03) 로그가 동일 [multi_match] unknown token [START_OBJECT] 재현; col:60=quotes, col:666=reviews 로 모두 동일 root cause Rejected (대표 신뢰 가능)

Fix Recommendation#

즉시 조치 (Critical)#

  • 코드 결함이 아니므로 긴급 수정 불필요. Error Tracking 에서 이 이슈는 IGNORE 권장. 발생은 전부 내부 dev/qa 테스트 트래픽의 malformed q 입력이다.

단기 개선 (1주 이내)#

  • q 파라미터 타입 검증 (선택): SearchableController#get_query_option / query_option (searchable_controller.rb:23,266) 또는 Cupix::QueryOption::Base#set_search_string (base.rb:138-166) 에서 @query_stringString 이 아니면 Cupix::Errors::Parameter (ARG10000/400) 로 거부한다. tesla 는 이미 .to_i/.split 이전 타입 검증을 관례로 사용한다(a21860c9 episode 참조). 이렇게 하면 malformed 입력이 ES 에 도달하기 전에 400 으로 차단되어 502 오분류가 사라진다. 프런트/클라이언트 응답코드 계약 조율이 필요하다.

장기 개선 (재발 방지)#

  • 컨트롤러 진입 시점 schema/contract 검증(scalar 여야 하는 검색 파라미터의 타입 강제)으로 q, sort 등 유사 파라미터의 malformed object/array 입력을 일괄 차단. sibling ARG13000 정렬 파라미터 오류(eb0195be)와 함께 검색 파라미터 입력 검증 계층을 정비.

Monitoring#

재발/배포 후 확인용 Datadog timeseries 쿼리:

text
service:cupixworks-api "unknown token [START_OBJECT] after"

502 로 표면화되는 검색 파라미터 오류 추적:

text
service:cupixworks-api "failed to parse field [must]" @http.status_code:502

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (코드 변경 없음 / 선택적 타입 검증 시 standard)

Noise Verdict#

noise — q 파라미터를 중첩 object 로 보낸 malformed client input(전부 내부 dev/qa 테스트 트래픽)이 원인이고 tesla 코드는 정상 동작하며 유일한 문제는 502 status-code 오분류라 코드 수정이 필요 없다.