📣 极限科技诚招搜索运维工程师(Elasticsearch/Easysearch)- 全职/北京 👉 : 立即申请加入

适用版本: 6.8-8.9

1. 错误说明 #

[wrapper] query malformed; expected 'query' but was <field_name> 是 Elasticsearch 在解析 wrapper 查询时抛出的 ParsingException。该错误表示 wrapper 对象的 JSON 结构中,解析器期望读取到名为 query 的字段,但实际读到的却是其他字段名,因此直接拒绝该查询。

常见现象 #

  • 发起搜索请求时,Elasticsearch 返回 400 Bad Request,响应体中包含 parsing_exceptionx_content_parse_exception
  • 错误日志中会出现类似 [wrapper] query malformed; expected 'query' but was source 的提示,其中 source 为实际读到的字段名。
  • 使用 REST API 直接提交 DSL 或使用 Java/Python 等客户端构造 wrapper 查询时均可能触发此异常。
  • 如果错误发生在批量请求或高频查询场景中,会导致大量请求失败,影响业务正常查询。

典型报错 #

{
  "error": {
    "root_cause": [
      {
        "type": "parsing_exception",
        "reason": "[wrapper] query malformed; expected `query` but was `source`"
      }
    ],
    "type": "parsing_exception",
    "reason": "[wrapper] query malformed; expected `query` but was `source`"
  },
  "status": 400
}

2. 原因分析 #

wrapper 查询是 Elasticsearch 中一种特殊的查询类型,它允许将一个已序列化的 JSON 查询(以 Base64 编码)作为 query 字段的值传入。其正确的结构如下:

{
  "wrapper": {
    "query": "eyAidGVybSI6IHsgIm5hbWUiOiAibmFtZSIgfSB9"
  }
}

其中 query 字段的值是一个 Base64 编码后的 JSON 字符串。

Elasticsearch 在解析 wrapper 对象时,会首先读取当前字段名,并严格校验它是否为 query。相关源码逻辑如下:

if (token != XContentParser.Token.FIELD_NAME) {
    throw new ParsingException(parser.getTokenLocation(), "[wrapper] query malformed");
}
String fieldName = parser.currentName();
if (QUERY_FIELD.match(fieldName, parser.getDeprecationHandler()) == false) {
    throw new ParsingException(
        parser.getTokenLocation(),
        "[wrapper] query malformed; expected `query` but was " + fieldName
    );
}
parser.nextToken();
byte[] source = parser.binaryValue();

常见导致该错误的原因包括:

  • 字段名拼写错误:将 query 误写为 sourcevaluebodydsl 等字段名。
  • JSON 结构层级错误wrapper 对象下嵌套了多余的层级,或者 query 字段被错误地嵌套在子对象中。
  • 上游代码拼接 DSL 错误:在动态构造查询时,错误地将其他字段名写入了 wrapper 对象的顶层。
  • 从其他查询类型错误迁移:将 boolfunction_score 等查询的结构直接套用到 wrapper 查询上,忽略了 wrapper 对字段名的严格要求。
  • Base64 编码前的原始 JSON 格式错误:虽然此错误直接报的是字段名问题,但有时也可能是因为整个 DSL 结构被错误包装,导致解析器读到了非预期的字段。

3. 解决方案 #

步骤一:确认当前 DSL 结构 #

首先将实际发送给 Elasticsearch 的 DSL 打印出来(可在客户端日志、网关日志或抓包中查看),确认 wrapper 部分的结构。正确结构应为:

{
  "query": {
    "wrapper": {
      "query": "eyAidGVybSI6IHsgIm5hbWUiOiAibmFtZSIgfSB9"
    }
  }
}

步骤二:修正字段名 #

如果字段名不是 query,将其修正。例如,以下错误写法:

{
  "query": {
    "wrapper": {
      "source": "eyAidGVybSI6IHsgIm5hbWUiOiAibmFtZSIgfSB9"
    }
  }
}

应修正为:

{
  "query": {
    "wrapper": {
      "query": "eyAidGVybSI6IHsgIm5hbWUiOiAibmFtZSIgfSB9"
    }
  }
}

步骤三:验证 Base64 编码内容 #

确认 query 字段的值是一个合法的 Base64 编码字符串,且解码后是合法的 Elasticsearch DSL。可以使用以下命令验证:

echo "eyAidGVybSI6IHsgIm5hbWUiOiAibmFtZSIgfSB9" | base64 -d

解码后应为标准 JSON,例如:

{ "term" : { "name" : "test" } }

步骤四:检查 Java/Python 客户端代码 #

如果使用的是 Java 客户端,确认代码如下:

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
WrapperQueryBuilder wrapperQuery = QueryBuilders.wrapperQuery(
    "{ \"term\" : { \"name\" : \"test\" } }"
);
sourceBuilder.query(wrapperQuery);

如果使用 Python 客户端:

import base64, json

dsl = json.dumps({"term": {"name": "test"}}).encode("utf-8")
encoded = base64.b64encode(dsl).decode("utf-8")

query = {
    "query": {
        "wrapper": {
            "query": encoded
        }
    }
}

4. 预防措施 #

  • 严格遵循官方文档结构:使用 wrapper 查询时,始终确保 wrapper 对象下第一个且唯一的字段名为 query
  • 在代码中封装 wrapper 构造方法:避免在每个调用处重复手写字段名,通过封装方法统一生成 wrapper 结构,减少拼写错误。
  • 在 CI 或单测中增加 DSL 结构校验:对动态生成的查询 DSL 做基本的 JSON schema 校验,提前发现字段名错误。
  • 借助 INFINI Gateway 进行请求审计:通过网关层记录所有发给 Elasticsearch 的请求体,当出现异常时可以快速回查原始 DSL,定位是客户端问题还是网关转发问题。
  • 谨慎使用 wrapper 查询wrapper 查询主要用于跨语言传递已序列化的查询,如果场景允许,优先考虑直接使用普通 DSL,减少一层 Base64 编码带来的复杂度和潜在错误。

借助 INFINI 产品提升排障效率 #

  • INFINI Console 适合查看集群健康度、索引状态、错误趋势和请求画像,帮助快速判断异常是 DSL 构造问题还是集群层面的问题。
  • INFINI Gateway 适合部署在 Elasticsearch 前面做请求观测、限流和流量治理,可以记录完整请求体,方便排查 DSL 格式错误。

5. 相关错误 #

附:日志上下文 #

if (token != XContentParser.Token.FIELD_NAME) {
    throw new ParsingException(parser.getTokenLocation(), "[wrapper] query malformed");
}
String fieldName = parser.currentName();
if (QUERY_FIELD.match(fieldName, parser.getDeprecationHandler()) == false) {
    throw new ParsingException(
        parser.getTokenLocation(),
        "[wrapper] query malformed; expected `query` but was " + fieldName
    );
}
parser.nextToken();
byte[] source = parser.binaryValue();