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#
- 2026-07-24 16:26 KST — 최초 발생:
GET /api/v1/admin/quotes(qa),q[number_eq]=...중첩 object 입력 (col:60) - 2026-08-03 20:47 ~ 21:20 KST —
GET /api/v1/reviews(dev),q[s]=published_at desc중첩 object 입력 버스트 (col:666) - 2026-08-07 00:14 KST — 최근 발생 (cluster last_seen)
- 2026-08-07 — RCA 분석 수행
Error Log#
[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_option 은 query_string: params[:q] 로 값을 타입 검증 없이 그대로 전달하고 (app/controllers/concerns/searchable_controller.rb:23), Cupix::QueryOption::Base#set_search_string 는 이 object 를 그대로 Elasticsearch multi_match 의 query 필드에 삽입한다 (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으로 전달:
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에 삽입:
@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는_search를begin블록 밖에서 호출하지만(base_repository.rb:71), lazy 실행이 어느 시점에 터지느냐에 따라 매핑이 달라진다.
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_controller의rescue_from Elasticsearch::Transport::Transport::Error → badgateway_on_elasticsearch_502_error, BG10002 로 매핑되는 경로) — lazy ES 예외가 base_repository begin 블록 밖에서 표면화됨. 이는 siblingARG13000 No mapping found ... in order to sort on(eb0195be episode) 과 동일한 매핑 계열이다.
Log Evidence#
Datadog query (재현):
service:cupixworks-api "unknown token [START_OBJECT] after"
reviews 발생의 request 로그 — q 가 중첩 object 임을 확증:
{
"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:
{
"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건):
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_string이String이 아니면Cupix::Errors::Parameter(ARG10000/400) 로 거부한다. tesla 는 이미.to_i/.split이전 타입 검증을 관례로 사용한다(a21860c9 episode 참조). 이렇게 하면 malformed 입력이 ES 에 도달하기 전에 400 으로 차단되어 502 오분류가 사라진다. 프런트/클라이언트 응답코드 계약 조율이 필요하다.
장기 개선 (재발 방지)#
- 컨트롤러 진입 시점 schema/contract 검증(scalar 여야 하는 검색 파라미터의 타입 강제)으로
q,sort등 유사 파라미터의 malformed object/array 입력을 일괄 차단. siblingARG13000정렬 파라미터 오류(eb0195be)와 함께 검색 파라미터 입력 검증 계층을 정비.
Monitoring#
재발/배포 후 확인용 Datadog timeseries 쿼리:
service:cupixworks-api "unknown token [START_OBJECT] after"
502 로 표면화되는 검색 파라미터 오류 추적:
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 오분류라 코드 수정이 필요 없다.