Hurl 过滤器(Filters)完全指南:在断言与捕获中精准转换 HTTP 响应数据
【免费下载链接】hurlHurl, run and test HTTP requests with plain text.项目地址: https://gitcode.com/GitHub_Trending/hu/hurl
Hurl 的过滤器(Filters)用于对查询(query)从 HTTP 响应中提取出的数据进行二次变换,是连接「提取」与「断言/捕获」的关键桥梁。本文以 docs/filters.md 为核心骨架,结合 Hurl 源码中的解析器与求值器实现,系统讲解过滤器的定义、链式用法、全部 27 个内置过滤器的功能与实战示例,帮助你在 Hurl 测试中完成对字符串、集合、字节、日期、URL 等数据的精准加工与验证。
什么是过滤器:从查询到断言的中间层
在 Hurl 中,捕获(Captures) 和 断言(Asserts) 共享同一套核心结构:查询(query)。查询用于从 HTTP 响应中提取数据,这些数据可以来自响应体(response body)、响应头(response headers),也可以是 HTTP 元信息(如duration时长)。
例如,下面两个片段中查询完全相同——都是jsonpath "$.books[0].name":
捕获(Capture)语法为
变量名: 查询:name: jsonpath "$.books[0].name"断言(Assert)语法为
查询 谓词:jsonpath "$.books[0].name" == "Dune"
两者复用同一个查询,说明查询是断言与捕获的公共基础。但很多时候,查询提取出的原始值并不能直接满足断言需求——比如提取的是一个逗号分隔的字符串,需要拆成列表再取第 0 个元素;或者提取的是 Base64 文本,需要先解码再比较。这时就需要过滤器(Filters)登场。
过滤器用于转换查询提取出的值,可以出现在断言和捕获中,对数据进行精炼。过滤器可以链式串联(chained),从而实现细粒度的数据提取。链式结构示例如下:
jsonpath "$.name" split "," nth 0 == "Herbert"这条断言的含义是:先用jsonpath提取$.name,再用split ","按逗号拆成列表,接着用nth 0取第一个元素,最后与"Herbert"比较。三个环节一目了然,完全由空格分隔,阅读顺序即执行顺序。
一个完整示例
下面这段 Hurl 文件综合演示了过滤器在捕获与断言中的典型用法:
GET https://example.org/api HTTP 200 [Captures] name: jsonpath "$.user.id" replaceRegex /\d/ "x" [Asserts] header "x-servers" split "," count == 2 header "x-servers" split "," nth 0 == "rec1" header "x-servers" split "," nth 1 == "rec3" jsonpath "$.books" count == 12- 在
[Captures]中,replaceRegex /\d/ "x"把$.user.id里的所有数字替换为x,净化后存入变量name; - 在
[Asserts]中,对响应头x-servers先split ","再分别取第 0、1 个元素做精确比较,并用count验证服务器节点数量; jsonpath "$.books" count == 12统计书籍数量。
过滤器的工作原理:从源码看语法解析与执行
要深入理解过滤器,可以从 Hurl 源码的实现链路入手。过滤器在 Hurl 中经历了「语法解析」与「运行时求值」两个阶段:
语法解析阶段:packages/hurl_core/src/parser/filter.rs中的filter()函数通过choice组合子逐一匹配过滤器关键字,并将结果封装为 AST 节点Filter { source_info, value }(定义见 packages/hurl_core/src/ast/core.rs#L270-L273)。filters()函数则负责解析连续多个过滤器——它会反复尝试「空白 + 过滤器」,一旦遇到空白不足或无法匹配的 token 就停止,从而支持链式写法:
loop { let space = zero_or_more_spaces(reader)?; if space.value.is_empty() { break; } match filter(reader) { /* 收集到 filters 列表 */ } }AST 中的FilterValue枚举(见 packages/hurl_core/src/ast/core.rs#L275-L349)完整定义了全部过滤器变体,例如Count、Nth { n }、ReplaceRegex { pattern, new_value }、ToDate { fmt }等。
运行时求值阶段:packages/hurl/src/runner/filter/eval.rs中的eval_filters()是链式执行的入口——它对输入值逐个应用过滤器,把上一个过滤器的输出作为下一个过滤器的输入,任一环节产生None都会立即报出FilterMissingInput错误:
let mut value = Some(value.clone()); for filter in filters { value = if let Some(value) = value { eval_filter(filter, &value, variables, in_assert, options)? } else { return Err(RunnerError::new(filter.source_info, RunnerErrorKind::FilterMissingInput, in_assert)); } }eval_filter()则按FilterValue枚举分发到packages/hurl/src/runner/filter/目录下的 27 个独立实现文件(如count.rs、nth.rs、replace_regex.rs、to_date.rs、xpath.rs等),每个文件对应一个过滤器,并自带单元测试。例如count.rs的eval_count()只接受列表、字节串或节点集(nodeset)输入,否则抛出FilterInvalidInputType错误。这套「解析器 → AST → 求值器 → 具体实现」的分层结构,使得过滤器的行为可预测、可单测、可组合。
过滤器速查表
下表完整列出了 Hurl 提供的全部过滤器及其输入输出类型(来源: docs/filters.md):
| 过滤器 | 说明 | 输入 | 输出 |
|---|---|---|---|
| base64Decode | 将 Base64 编码字符串解码为字节 | string | bytes |
| base64Encode | 将字节编码为 Base64 字符串 | bytes | string |
| base64UrlSafeDecode | 按 Base64 URL-safe 编码规则解码字符串为字节 | string | bytes |
| base64UrlSafeEncode | 按 Base64 URL-safe 编码规则将字节编码为字符串 | bytes | string |
| charsetDecode | 使用指定字符集编码将字节解码为字符串 | bytes | string |
| count | 统计集合中元素个数 | collection | number |
| dateFormat | 按规范格式将日期格式化为字符串 | date | string |
| daysAfterNow | 返回现在与未来某日期之间相差的天数 | date | number |
| daysBeforeNow | 返回现在与过去某日期之间相差的天数 | date | number |
| first | 返回集合中第一个元素 | collection | any |
| htmlEscape | 将&、<、>转换为 HTML 安全序列 | string | string |
| htmlUnescape | 将命名/数字字符引用(如>、>、>)转换为 Unicode 字符 | string | string |
| jsonpath | 对字符串求值 JSONPath 表达式 | string | any |
| last | 返回集合中最后一个元素 | collection | any |
| location | 返回重定向的目标 URL | response | string |
| nth | 返回集合中指定下标的元素,支持负索引从尾部计数 | collection | any |
| regex | 提取正则捕获组,模式必须至少含一个捕获组 | string | string |
| replace | 将旧字符串全部替换为新字符串 | string | string |
| replaceRegex | 将匹配正则模式的内容全部替换为新字符串 | string | string |
| split | 按指定分隔符将字符串拆成列表 | string | list |
| toDate | 按规范格式将字符串转换为日期 | string | date |
| toFloat | 将值转换为浮点数 | string | number | number |
| toHex | 将字节转换为十六进制字符串 | bytes | string |
| toInt | 将值转换为整数 | string | number | number |
| toString | 将值转换为字符串 | any | string |
| urlDecode | 将 %xx 转义替换为对应的单字符 | string | string |
| urlEncode | 对非保留字符(RFC3986 定义)之外的所有字符做百分号编码,斜杠/除外 | string | string |
| urlQueryParam | 返回 URL 中指定查询参数的值 | string | string |
| utf8Decode | 按 UTF-8 编码将字节解码为字符串 | bytes | string |
| utf8Encode | 按 UTF-8 编码将字符串编码为字节 | string | bytes |
| xpath | 对字符串求值 XPath 表达式 | string | string |
以下逐一给出每个过滤器的语义说明与可直接复用的 Hurl 示例。
编码与字节类过滤器
base64Decode
将 Base64 编码字符串解码为字节。适合对响应体中的 Base64 字段做二进制级校验:
GET https://example.org/api HTTP 200 [Asserts] jsonpath "$.token" base64Decode == hex,3c3c3f3f3f3e3e;base64Encode
将字节编码为 Base64 字符串,常与base64Decode或bytes查询配合实现「解码 → 再编码」的往返验证:
GET https://example.org/api HTTP 200 [Asserts] bytes base64Encode == "PDw/Pz8+Pg=="base64UrlSafeDecode
按 Base64 URL-safe 编码规则(RFC 4648 第 5 节,-与_替代+与/)解码字符串为字节:
GET https://example.org/api HTTP 200 [Asserts] jsonpath "$.token" base64UrlSafeDecode == hex,3c3c3f3f3f3e3e;base64UrlSafeEncode
将字节编码为 Base64 URL-safe 字符串。注意标准 Base64 与 URL-safe 两种编码的结果可能不同(示例中标准编码为"PDw/Pz8+Pg==",URL-safe 编码为"PDw_Pz8-Pg"):
GET https://example.org/api HTTP 200 [Asserts] bytes base64UrlSafeEncode == "PDw_Pz8-Pg"charsetDecode
旧名为decode,已弃用,将在未来主版本中移除。
使用指定字符集编码将字节解码为字符串。字符集标签遵循 Encoding Standard 定义。典型场景是响应体未在Content-Type中声明字符集时,需手动解码后才能做文本断言:
# 响应头 'Content-Type' 未声明字符集 'gb2312' # 因此 Hurl 必须先显式解码响应体,才能进行任何基于文本的断言 GET https://example.org/hello_china HTTP 200 [Asserts] header "Content-Type" == "text/html" # Content-Type 未携带编码信息,我们必须自己解码响应体 bytes charsetDecode "gb2312" xpath "string(//body)" == "你好世界"当编码为 UTF-8(即charsetDecode "utf-8")时,可直接改用 utf8Decode 过滤器。
toHex
将字节转换为十六进制字符串,用于查看或比对二进制内容:
GET https://example.org/foo HTTP 200 [Asserts] bytes toHex == "d188d0b5d0bbd0bbd18b"utf8Decode
按 UTF-8 编码将字节解码为字符串。一个常见组合是「Base64 解码 → UTF-8 解码」,还原完整文本:
GET https://example.org/messages HTTP 200 [Asserts] # 从 Base64 字符串到 UTF-8 字节再到最终字符串 jsonpath "$.bytesInBase64" base64Decode utf8Decode == "Hello World"utf8Encode
按 UTF-8 编码将字符串编码为字节,常与toHex串联以断言字节序列:
GET https://example.org/drinks HTTP 200 [Asserts] jsonpath "$.beverage" utf8Encode toHex == "636166C3A9"集合处理类过滤器
count
统计集合中元素的个数。这里的「集合」在源码实现(packages/hurl/src/runner/filter/count.rs)中明确支持列表(list)、字节串(bytes)与节点集(nodeset)三类输入;对其它类型会报FilterInvalidInputType错误,期望类型为list, bytes or nodeset:
GET https://example.org/api HTTP 200 [Asserts] jsonpath "$.books" count == 12first
返回集合中的第一个元素:
GET https://example.org HTTP 200 [Asserts] jsonpath "$.books" first == "Dune"last
返回集合中的最后一个元素:
GET https://example.org HTTP 200 [Asserts] jsonpath "$.books" last == "Les Misérables"nth
返回集合中指定下标的元素,下标从 0 开始,支持负索引(从集合尾部倒序计数)。从源码实现(packages/hurl/src/runner/filter/nth.rs)可以看到,try_nth()对index >= 0 && index < len取正向下标,对index < 0 && len - index.abs() >= 0取len - index.abs()的倒序下标,越界时抛出out of bound - size is N错误;下标参数本身支持字面量或{{变量}}占位符:
GET https://example.org/api HTTP 200 [Asserts] jsonpath "$.books" nth 2 == "Children of Dune"负索引示例(取列表最后一个元素):
[Asserts] jsonpath "$.list" nth -1 == 3split
按指定分隔符将字符串拆分为字符串列表,是「先拆分、后计数/取元素」类断言的常用前置过滤器:
GET https://example.org/foo HTTP 200 [Asserts] jsonpath "$.ips" split ", " count == 3字符串处理类过滤器
regex
按正则模式提取捕获组内容。模式必须至少包含一个捕获组,支持三种写法:双引号字符串、/.../字面量与带内联标志的/.../(如(?i)忽略大小写):
GET https://example.org/foo HTTP 200 [Captures] param1: header "header1" param2: header "header2" regex "Hello (.*)!" param3: header "header2" regex /Hello (.*)!/ param3: header "header2" regex /(?i)Hello (.*)!/Hurl 使用的正则语法以 Rustregexcrate 为准,支持非贪婪匹配、字符类等特性;注意它并非 PCRE 语法,部分回溯型构造不可用。
replace
将字符串中所有出现的旧字符串替换为新字符串。注意:第一个参数是普通字符串而非正则,这一点与replaceRegex有本质区别:
GET https://example.org/foo HTTP 200 [Captures] url: jsonpath "$.url" replace "http://" "https://" [Asserts] jsonpath "$.ips" replace ", " "|" == "192.168.2.1|10.0.0.20|10.0.0.10"replaceRegex
将字符串中所有匹配正则模式的内容替换为新字符串。模式支持字符串形式或/.../字面量形式,也支持{{变量}}模板。源码实现(packages/hurl/src/runner/filter/replace_regex.rs)显示其基于 Rustregex的replace_all完成全部替换:
GET https://example.org/foo HTTP 200 [Captures] url: jsonpath "$.id" replaceRegex /\d/ "x" [Asserts] jsonpath "$.message" replaceRegex "B[aoi]b" "Dude" == "Welcome Dude!"htmlEscape
将字符&、<、>转换为 HTML 安全序列(&、<、>):
GET https://example.org/api HTTP 200 [Asserts] jsonpath "$.text" htmlEscape == "a > b"htmlUnescape
将命名与数字字符引用(如>、>、>)还原为对应的 Unicode 字符,是htmlEscape的逆操作:
GET https://example.org/api HTTP 200 [Asserts] jsonpath "$.escaped_html[1]" htmlUnescape == "Foo © bar 𝌆"URL 处理类过滤器
location
返回重定向的目标 URL。与来源Location响应头不同,location过滤器返回的 URL始终是绝对地址(Location头可能是相对地址)。源码实现(packages/hurl/src/runner/filter/location.rs)要求输入为HttpResponse类型值,通常配合redirects查询使用:
GET https://example.org/step1 [Options] location: true HTTP 200 [Asserts] redirects count == 2 redirects nth 0 location == "https://example.org/step2" redirects nth 1 location == "https://example.org/step3"urlDecode
将%xx百分号转义替换为其对应的单字符:
GET https://example.org/foo HTTP 200 [Asserts] jsonpath "$.encoded_url" urlDecode == "https://mozilla.org/?x=шеллы"urlEncode
对不在非保留字符集合(见 RFC3986)内的所有字符做百分号编码,但斜杠/除外:
GET https://example.org/foo HTTP 200 [Asserts] jsonpath "$.url" urlEncode == "https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"可以看到https://中的/保持原样,其余非保留字符均被编码。
urlQueryParam
返回 URL 中指定查询参数的值,参数缺失时配合not exists谓词可验证参数不存在性:
GET https://example.org/foo HTTP 200 [Asserts] jsonpath "$.url" urlQueryParam "x" == "шеллы"类型转换类过滤器
toDate
按规范格式字符串将字符串转换为日期。格式语法与 Rustchronocrate 的 strftime 一致。源码实现(packages/hurl/src/runner/filter/to_date.rs)采用启发式解析:依次尝试「日期+时间+时区 → 日期+时间 → 仅日期」三种粒度,兼容带时区、不带时区乃至纯日期的输入:
GET https:///example.org HTTP 200 [Asserts] header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" daysBeforeNow > 1000ISO 8601 / RFC 3339 日期时间格式有快捷写法%+,下面示例同时演示了toDate与dateFormat的串联,以及%.f解析小数秒:
GET https://example.org/api/books HTTP 200 [Asserts] jsonpath "$.published" == "2023-01-23T18:25:43.511Z" jsonpath "$.published" toDate "%Y-%m-%dT%H:%M:%S%.fZ" dateFormat "%A" == "Monday" jsonpath "$.published" toDate "%+" dateFormat "%A" == "Monday" # %+ 可用于解析 ISO 8601 / RFC 3339dateFormat
旧名为format,已弃用,将在未来主版本中移除。
按规范格式将日期格式化为字符串,通常与toDate或cookie/certificate查询组合。在源码的 entry.rs 中可以看到,Format与Decode这两个旧名过滤器被显式检测并用于输出弃用告警:
GET https://example.org HTTP 200 [Asserts] cookie "LSID[Expires]" dateFormat "%a, %d %b %Y %H:%M:%S" == "Wed, 13 Jan 2021 22:23:01"daysAfterNow
返回当前时间与某个未来日期之间相差的天数。适合验证证书或令牌的剩余有效期:
GET https://example.org HTTP 200 [Asserts] certificate "Expire-Date" daysAfterNow > 15daysBeforeNow
返回当前时间与某个过去日期之间相差的天数:
GET https://example.org HTTP 200 [Asserts] certificate "Start-Date" daysBeforeNow < 100toFloat
将值转换为浮点数,输入可以是字符串或数字:
GET https://example.org/foo HTTP 200 [Asserts] jsonpath "$.pi" toFloat == 3.14toInt
将值转换为整数,输入可以是字符串或数字(浮点字符串如"1.6"转整数时取整):
GET https://example.org/foo HTTP 200 [Asserts] jsonpath "$.id" toInt == 123toString
将任意值转换为字符串,便于与字符串字面量比较:
GET https://example.org/foo HTTP 200 [Asserts] jsonpath "$.count" toString == "42"结构化数据查询类过滤器
jsonpath
对字符串输入求值 JSONPath 表达式。常用于对「捕获到的 JSON 字符串」或「从 XPath 取出的内嵌 JSON」做二次提取:
GET https://example.org/api HTTP 200 [Captures] books: xpath "string(//body/@data-books)" [Asserts] variable "books" jsonpath "$[0].name" == "Dune" variable "books" jsonpath "$[0].author" == "Franck Herbert"xpath
对字符串输入求值 XPath 表达式,是处理 HTML/XML 响应的利器,也可与charsetDecode组合应对非 UTF-8 页面:
GET https://example.org/hello_gb2312 HTTP 200 [Asserts] bytes charsetDecode "gb2312" xpath "string(//body)" == "你好世界"链式组合实战:从集成测试看过滤器的真实用法
仓库集成测试 integration/hurl/tests_ok/filter/filter.hurl 是对全部过滤器最完整的实战验证,以下提炼几个代表性组合:
1. 解码与再编码的往返验证(Base64 ↔ 字节):
jsonpath "$.file" base64Decode == hex,d188d0b5d0bbd0bbd18b; jsonpath "$.file" base64Decode base64Encode == "0YjQtdC70LvRiw==" jsonpath "$.text_encoded" base64UrlSafeDecode base64Encode == "PDw/Pz8+Pg=="2. 集合三件套(count/first/last/nth及负索引):
jsonpath "$.list" count == 3 jsonpath "$.list" first == 1 jsonpath "$.list" last == 3 jsonpath "$.list" nth 1 == 2 jsonpath "$.list" nth -1 == 33. URL 编码往返(urlEncode与urlDecode互为逆操作,配合捕获的变量做完整性校验):
jsonpath "$.url" == "https://mozilla.org/?x=шеллы" jsonpath "$.url" urlEncode == "https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" jsonpath "$.encoded_url" urlDecode == "https://mozilla.org/?x=шеллы" variable "url" urlEncode urlDecode == "{{url}}"4. HTML 转义往返:
jsonpath "$.text" htmlEscape == "a > b && a < c" jsonpath "$.escaped_html[1]" htmlUnescape == "Foo © bar 𝌆 baz ☃ qux" variable "text" htmlEscape htmlUnescape == "{{text}}"5. 类型转换链(字符串 → 数值 → 字符串):
jsonpath "$.score" toInt == 1 jsonpath "$.score" toFloat == 1.6 jsonpath "$.score" toString == "1.6"6. 正则替换的普通字符串与正则对比——replace的第一个参数是字面量,\d不会被当作正则,因此结果保持"123";而replaceRegex会真正匹配数字:
# replace 的第一个参数不是正则 jsonpath "$.id" replace "\\d" "x" == "123" jsonpath "$.id" replaceRegex /\d/ "x" == "xxx" jsonpath "$.message" replaceRegex /(?i)b[a-z]b/ "dude" == "Hello dude!"7. 模板变量驱动过滤器参数——过滤器的参数同样支持{{变量}}占位符,可结合[Options]中的variable动态注入模式与索引:
GET http://localhost:8000/filter [Options] variable: single_digit=\d variable: index_first=1 variable: index_last=-1 HTTP 200 [Asserts] jsonpath "$.id" replaceRegex "{{single_digit}}" "x" == "xxx" jsonpath "$.list" nth {{index_first}} == 2 jsonpath "$.list" nth {{index_last}} == 3使用要点与注意事项
- 链式顺序即执行顺序:过滤器从左到右依次执行,前一过滤器的输出类型必须是后一过滤器的期望输入类型(见速查表)。若输入类型不匹配,Hurl 会抛出
FilterInvalidInputType错误,并明确指出实际类型与期望类型(例如count期望list, bytes or nodeset)。 - 越界与非法输入会报错:
nth越界会报out of bound - size is N;base64Decode收到非法 Base64 字符串会报string is not base64;regex模式缺少捕获组会导致断言失败。 - 弃用别名:
decode与format分别是charsetDecode与dateFormat的旧名,已标记弃用并将在未来主版本移除,建议新代码直接使用新名称。 %+快捷格式:toDate支持用%+直接解析 ISO 8601 / RFC 3339 时间戳,可省去手写%Y-%m-%dT%H:%M:%S%.fZ这类复杂格式。- 模板化参数:过滤器参数(正则模式、替换串、
nth索引、字符集名、URL 查询参数名等)均支持{{变量}}模板,使同一份 Hurl 文件可通过 变量注入 在不同环境间复用。
小结
过滤器是 Hurl 中把「查询提取」与「断言/捕获」粘合起来的能力层:27 个内置过滤器覆盖了 Base64/字符集/UTF-8 编解码、集合索引与计数、字符串拆分与正则替换、HTML 转义、URL 编解码与参数提取、类型转换与日期运算、JSONPath/XPath 二次查询等常见数据加工场景。结合链式语法,你几乎可以在一条断言里完成「提取 → 拆解 → 筛选 → 比较」的全流程。若要进一步查阅过滤器的语法细节与文件结构,可继续阅读 docs/filters.md、断言文档、捕获文档,以及源码中的 过滤器求值器 与 集成测试。
【免费下载链接】hurlHurl, run and test HTTP requests with plain text.项目地址: https://gitcode.com/GitHub_Trending/hu/hurl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考