与 HandlerStack 完全指南:从自定义中间件到重试、日志与错误处理)
后端【免费下载链接】guzzleGuzzle, an extensible PHP HTTP client项目地址https://gitcode.com/gh_mirrors/gu/guzzle点击查看免费下载导读本指南以 Guzzle 官方文档 docs/middleware.md 为核心骨架系统讲解 Guzzle可扩展的 PHP HTTP 客户端中中间件Middleware与处理器栈HandlerStack的完整机制。你将掌握中间件的高阶函数编写范式、请求/响应映射、Tap 观察、日志记录、HTTP 错误定制、重试策略以及 HandlerStack 的组合、命名与增删排序技巧并深入底层源码理解其 Promise 化的工作原理。读完本文你可以为生产环境打造可复用的请求日志、链路追踪、指数退避重试等中间件体系。一、中间件是什么理解 Guzzle 的可插拔请求管道Guzzle 客户端并不直接发送 HTTP 请求而是把“真正把请求发到网络”的工作委托给一个处理器Handler。处理器是一个接收请求并返回 Promise 的函数。为了让客户端具备重定向、Cookie、鉴权、错误抛出等丰富行为Guzzle 用一层层**中间件Middleware**包裹wrap这个处理器形成一个洋葱式的调用链。相关的处理器概念参见 docs/handlers.md。中间件本质上是一个高阶函数higher order function它接收“下一个要调用的处理器”返回一个新的、经过包装的处理器。其标准形式如下use Psr\Http\Message\RequestInterface; function my_middleware() { return function (callable $handler) { return function (RequestInterface $request, array $options) use ($handler) { return $handler($request, $options); }; }; }拆解这三层闭包最外层my_middleware()是一个工厂函数用来捕获你的自定义参数比如下面的$header、$value中间层接收$handler下一个处理器或下一层中间件包装后的函数内层则真正扮演“组合后的处理器”接收Psr\Http\Message\RequestInterface与请求选项数组$options并返回一个以Psr\Http\Message\ResponseInterface兑现的 Promise即GuzzleHttp\Promise\PromiseInterface。你的中间件可以在调用下游处理器之前修改请求、新增自定义请求选项也可以在拿到下游返回的 Promise 之后修改响应。所有内置中间件的工厂方法都定义在 src/Middleware.php 的GuzzleHttp\Middleware类中此类是final且私有构造函数全部通过public static工厂方法使用。二、编写第一个中间件给每个请求添加 Header下面示例展示如何编写一个给所有请求添加指定 Header 的中间件use Psr\Http\Message\RequestInterface; function add_header($header, $value) { return function (callable $handler) use ($header, $value) { return function ( RequestInterface $request, array $options ) use ($handler, $header, $value) { $request $request-withHeader($header, $value); return $handler($request, $options); }; }; }创建中间件后有两种方式接入客户端包装客户端使用的处理器即把中间件应用到HandlerStack上再传给Client的handler构造选项装饰一个处理器栈直接操作HandlerStack实例。推荐的做法是通过HandlerStackuse GuzzleHttp\HandlerStack; use GuzzleHttp\Handler\CurlHandler; use GuzzleHttp\Client; $stack new HandlerStack(); $stack-setHandler(new CurlHandler()); $stack-push(add_header(X-Foo, bar)); $client new Client([handler $stack]);此后客户端发出的每一个请求都会在真正发送前被注入X-Foo: bar头。注意new HandlerStack()后必须调用setHandler()指定底层处理器否则在resolve()时会抛出LogicException见 src/HandlerStack.php。三、修改响应在 Promise 上挂接回调中间件不仅能改请求还能通过 Promise 的then()回调修改响应。下面示例给响应添加一个 Headeruse Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use GuzzleHttp\HandlerStack; use GuzzleHttp\Handler\CurlHandler; use GuzzleHttp\Client; function add_response_header($header, $value) { return function (callable $handler) use ($header, $value) { return function ( RequestInterface $request, array $options ) use ($handler, $header, $value) { $promise $handler($request, $options); return $promise-then( function (ResponseInterface $response) use ($header, $value) { return $response-withHeader($header, $value); } ); }; }; } $stack new HandlerStack(); $stack-setHandler(new CurlHandler()); $stack-push(add_response_header(X-Foo, bar)); $client new Client([handler $stack]);由于响应可能在网络传输中尚未完成Guzzle 中间件从不直接“返回响应对象”而是始终操作 Promise——这正是 Guzzle 能够同时支持同步wait()与异步Promise 链两种调用方式的原因。四、Middleware::mapRequest()与Middleware::mapResponse()更简洁的映射写法手工书写三层闭包比较繁琐GuzzleHttp\Middleware提供了两个内置中间件简化请求/响应修改Middleware::mapRequest(callable $fn)接收一个“入参为请求、返回值为请求”的函数。源码实现见 src/Middleware.phpreturn $handler($fn($request), $options);即先对请求做映射再转发。Middleware::mapResponse(callable $fn)接收一个“入参为响应、返回值为响应”的函数。源码实现为return $handler($request, $options)-then($fn);见 src/Middleware.php即在下游 Promise 兑现后对响应做映射。修改请求的示例use Psr\Http\Message\RequestInterface; use GuzzleHttp\HandlerStack; use GuzzleHttp\Handler\CurlHandler; use GuzzleHttp\Client; use GuzzleHttp\Middleware; $stack new HandlerStack(); $stack-setHandler(new CurlHandler()); $stack-push(Middleware::mapRequest(function (RequestInterface $request) { return $request-withHeader(X-Foo, bar); })); $client new Client([handler $stack]);修改响应的示例use Psr\Http\Message\ResponseInterface; use GuzzleHttp\HandlerStack; use GuzzleHttp\Handler\CurlHandler; use GuzzleHttp\Client; use GuzzleHttp\Middleware; $stack new HandlerStack(); $stack-setHandler(new CurlHandler()); $stack-push(Middleware::mapResponse(function (ResponseInterface $response) { return $response-withHeader(X-Foo, bar); })); $client new Client([handler $stack]);mapRequest与mapResponse均被 tests/MiddlewareTest.php 中的大量用例覆盖可放心用于生产代码。五、Tap 中间件只观察、不干预GuzzleHttp\Middleware::tap()用于“观察”请求与响应在栈中流动的过程而不修改任何东西非常适合埋点、追踪与调试。其源码src/Middleware.php在转发前后分别调用可选回调然后原样返回下游 Promise。use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; use GuzzleHttp\Promise\PromiseInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; $stack HandlerStack::create(); $stack-push(Middleware::tap( function (RequestInterface $request, array $options) { echo Sending . $request-getMethod() . . $request-getUri() . \n; }, function (RequestInterface $request, array $options, PromiseInterface $promise) { $promise-then(function (ResponseInterface $response) { echo Received . $response-getStatusCode() . \n; }); } )); $client new Client([handler $stack]); $client-request(GET, https://example.com);tap()的两个可选回调约定$before以$before($request, $options)形式在请求转发给下一个处理器之前调用$after以$after($request, $options, $promise)形式在请求转发之后立即调用。注意它收到的是响应Promise而非响应对象因为传输可能还在进行中若需读取最终响应请在$after中对 Promise 挂then()回调。两个回调的返回值均被忽略Tap 中间件无法改变请求、选项或响应。这套“只读观察”语义保证了它不会干扰正常请求链路。六、Logging 中间件结构化请求日志GuzzleHttp\Middleware::log()使用GuzzleHttp\MessageFormatterInterface实现记录请求、响应与错误public static function log( Psr\Log\LoggerInterface $logger, GuzzleHttp\MessageFormatterInterface $formatter, string $logLevel info ): callable参数约定见 src/Middleware.php 源码$logger任意 PSR-3 日志器Psr\Log\LoggerInterface实现$formatter负责把请求/响应/异常格式化为一行日志文本$logLevel成功响应的日志级别默认info失败传输Promise 被拒绝时固定使用error级别记录与$logLevel无关若拒绝原因是ResponseException如 4xx/5xx 响应异常格式化时会把其中的响应一并传入。使用示例use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use GuzzleHttp\MessageFormatter; use GuzzleHttp\Middleware; // $logger is any Psr\Log\LoggerInterface implementation. $stack HandlerStack::create(); $stack-push(Middleware::log($logger, new MessageFormatter(MessageFormatter::DEBUG), debug)); $client new Client([handler $stack]); $client-request(GET, https://example.com);6.1 MessageFormatter 模板与占位符GuzzleHttp\MessageFormatter源码见 src/MessageFormatter.php从一个模板字符串出发把{placeholder}占位符替换为请求、响应与错误中的实际值。它内置三套预设模板MessageFormatter::CLF— Apache 通用日志格式Common Log Format当构造时不传模板时即为默认值MessageFormatter::DEBUG— 请求、响应及错误的完整转储MessageFormatter::SHORT— 简短的单行摘要。// 使用预设模板... $formatter new MessageFormatter(MessageFormatter::SHORT); // ...或自定义模板。 $formatter new MessageFormatter({method} {uri} {code});以下为支持的占位符全表与 src/MessageFormatter.php 的文档注释一致占位符说明{request}完整 HTTP 请求消息{response}完整 HTTP 响应消息{req_headers}请求起始行与请求头{res_headers}响应起始行与响应头{req_body}请求体{res_body}响应体{method}请求方法{uri}/{url}请求的 URI{target}请求目标路径、查询与 fragment{host}请求的Host头{hostname}发出请求的机器的主机名gethostname(){version}/{req_version}请求的协议版本{res_version}响应的协议版本{code}响应状态码{phrase}响应原因短语Reason phrase{error}错误信息如有{ts}/{date_iso_8601}GMT 时区的 ISO 8601 日期{date_common_log}配置时区下的 Apache Common Log 日期{req_header_*}单个请求头把*替换为头名称{res_header_*}单个响应头把*替换为头名称例如内置的CLF模板实际为{hostname} {req_header_User-Agent} - [{date_common_log}] {method} {target} HTTP/{version} {code} {res_header_Content-Length}格式化实现通过preg_replace_callback逐占位符替换并缓存结果src/MessageFormatter.php注意{res_body}在响应体不可 seek 时输出RESPONSE_NOT_LOGGEABLE占位文本避免破坏流式响应。[!WARNING] 包含完整消息、头、体、URI、URL 或动态头占位符的模板可能泄露敏感数据如凭据、Cookie、令牌或请求体。生产环境应避免使用 debug 或完整消息模板除非日志受到保护更稳妥的做法是提供自定义 formatter 或日志处理器processor在日志写入前对敏感数据做脱敏。七、定制 HTTP 错误消息BodySummarizer当http_errors请求选项开启时http_errors中间件会对4xx响应抛出GuzzleHttp\Exception\ClientException对5xx响应抛出GuzzleHttp\Exception\ServerException并在异常消息中包含响应体的简短摘要。其核心逻辑在 src/Middleware.php状态码 400直接放行否则调用RequestException::create($request, $response, null, $bodySummarizer)抛出异常。GuzzleHttp\Middleware::httpErrors()接受一个可选的GuzzleHttp\BodySummarizerInterface来控制响应体摘要方式。仓库自带的GuzzleHttp\BodySummarizer见 src/BodySummarizer.php接受一个可选字节数上限$truncateAt传入整数可限制异常消息中出现的响应体长度也可以自行实现BodySummarizerInterface获得完全控制权。这对于避免把大型或二进制响应体写入异常消息与日志非常有用。相关截断行为在 tests/MiddlewareTest.php 中有测试佐证例如 1000 字节响应体在默认上限下被截断到 120 字节并附带(truncated...)标记。默认处理器栈中该中间件注册名为http_errors且是第一个被 push 的中间件因此处于最外层最先接收请求、最后处理响应。要保留其位置并更换摘要器可以先remove再unshift替换use GuzzleHttp\BodySummarizer; use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; $stack HandlerStack::create(); $stack-remove(http_errors); $stack-unshift(Middleware::httpErrors(new BodySummarizer(500)), http_errors); $client new Client([handler $stack]);八、Retry 中间件自定义重试策略GuzzleHttp\Middleware::retry()用于在自定义判定函数decider返回true时重试请求。其实现封装在 src/RetryMiddleware.php工厂方法见 src/Middleware.php。8.1 decider 判定函数decider 接收当前重试次数、请求、成功时的响应若有以及失败传输的拒绝原因rejection reasonfunction ( int $retries, RequestInterface $request, ?ResponseInterface $response null, $reason null ): bool拒绝原因本身可能暴露响应例如它是ResponseException时。保守的重试策略应只重试连接建立错误ConnectException与“请求过多”HTTP 429错误use GuzzleHttp\Client; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; $stack HandlerStack::create(); $stack-push(Middleware::retry( function ( int $retries, RequestInterface $request, ?ResponseInterface $response null, $reason null ): bool { if ($retries 3) { return false; } if ($reason instanceof ConnectException) { return true; } return $response ! null $response-getStatusCode() 429; }, function ( int $retries, ?ResponseInterface $response, RequestInterface $request ): int { return 1000 * $retries; } )); $client new Client([handler $stack]); $response $client-request(GET, https://example.com);decider 判定成功返回falsefulfilled 分支原样返回响应rejected 分支原样重抛拒绝原因判定重试返回true则进入doRetry()递增retries并再次调用自身见 src/RetryMiddleware.php。8.2 retries 选项与 delay 回调重试中间件把当前重试次数记录在retries请求选项中首次尝试前初始化为0每次重试前递增自定义中间件可以读取该选项也可以按请求单独播种初始值$response $client-request(GET, https://example.com, [ retries 1, ]);retries选项必须是整数否则抛出InvalidArgumentException见 src/RetryMiddleware.php。若未提供 delay 回调中间件使用指数退避作为默认延迟并把计算出的等待毫秒数写入delay请求选项后再发起下一次尝试。默认实现为(int) ((2 ** ($retries - 1)) * 1000)src/RetryMiddleware.php即第 1 次重试等待 1000ms、第 2 次 2000ms、第 3 次 4000ms……delay 回调必须返回整数毫秒数。相关选项常量定义见 src/RequestOptions.php 与RequestOptions::DELAY。九、HandlerStack中间件的组合与编排HandlerStack表示作用于底层处理器函数上的一层中间件“栈”。你可以push()把中间件加到栈顶部unshift()把中间件加到栈底部解析resolve时处理器先入栈然后每个中间件依次出栈并包装前一个出栈的值——最终形成一个洋葱式组合。use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; use GuzzleHttp\Utils; use Psr\Http\Message\RequestInterface; $stack new HandlerStack(); $stack-setHandler(Utils::chooseHandler()); $stack-push(Middleware::mapRequest(function (RequestInterface $r) { echo A; return $r; })); $stack-push(Middleware::mapRequest(function (RequestInterface $r) { echo B; return $r; })); $stack-push(Middleware::mapRequest(function (RequestInterface $r) { echo C; return $r; })); $client-request(GET, http://httpbin.org/); // echoes ABC; $stack-unshift(Middleware::mapRequest(function (RequestInterface $r) { echo 0; return $r; })); $client new Client([handler $stack]); $client-request(GET, http://httpbin.org/); // echoes 0ABC;这个示例直观展示了执行顺序push依次入栈后最早push 的中间件位于最外层、最先执行unshift则插入到栈底——从执行序看它排在最前面因此先输出0。9.1 中间件命名before / after / remove可以给中间件起名字从而支持“在某命名中间件之前/之后插入”以及“按名字移除”use Psr\Http\Message\RequestInterface; use GuzzleHttp\Middleware; // 给中间件命名 $stack-push(Middleware::mapRequest(function (RequestInterface $r) { return $r-withHeader(X-Foo, Bar); }), add_foo); // 在命名中间件之前添加相当于 unshift 到该位置之前 $stack-before(add_foo, Middleware::mapRequest(function (RequestInterface $r) { return $r-withHeader(X-Baz, Qux); }), add_baz); // 在命名中间件之后添加相当于 push 到该位置之后 $stack-after(add_baz, Middleware::mapRequest(function (RequestInterface $r) { return $r-withHeader(X-Lorem, Ipsum); }), add_lorem); // 按名字移除中间件 $stack-remove(add_foo);相关实现src/HandlerStack.phpbefore/after通过splice()在指定位置插入元组remove支持按名字字符串或可调用实例两种方式过滤按名字查找不到时会抛出InvalidArgumentException(Middleware not found: ...)src/HandlerStack.php。9.2 默认处理器栈推荐使用HandlerStack::create()创建带默认中间件的栈src/HandlerStack.php它等价于$stack new HandlerStack($handler ?: Utils::chooseHandler()); $stack-push(Middleware::httpErrors(), http_errors); // 4xx/5xx 抛异常最外层 $stack-push(Middleware::redirect(), allow_redirects); // 跟随重定向 $stack-push(Middleware::auth(), auth); // Basic/Digest 鉴权 $stack-push(Middleware::cookies(), cookies); // Cookie 处理 $stack-push(Middleware::prepareBody(), prepare_body); // 请求体准备最内层发送请求时自外向内依次经过http_errors → allow_redirects → auth → cookies → prepare_body → 底层处理器处理响应时顺序反转prepare_body不参与、cookies提取响应 Cookie、auth处理 Digest 挑战、allow_redirects跟随跳转、最后由http_errors对状态码 400的响应抛异常完整顺序见 docs/handlers.md。这正是大多数请求选项http_errors、allow_redirects、auth、cookies、expect等能够生效的基础——若自定义handler不包含对应中间件相应请求选项将不产生效果。十、其他内置中间件一览GuzzleHttp\Middleware还提供以下常用工厂方法源码见 src/Middleware.php它们同样遵循“接收 handler、返回包装后 handler”的契约工厂方法作用Middleware::auth(bool $reuseChallenges true)在设置auth请求选项时应用内置 Basic 认证并处理 Digest 认证挑战封装为AuthMiddlewaresrc/AuthMiddleware.phpMiddleware::cookies()设置cookies选项须为CookieJarInterface实例时向请求添加 Cookie 头、从响应提取 Cookie 到 Cookie JarMiddleware::history($container)把每次传输的request、response、error、options记录到数组或ArrayAccess容器失败时response为null容器不合法时抛InvalidArgumentExceptionMiddleware::redirect()处理重定向封装为RedirectMiddlewaresrc/RedirectMiddleware.phpMiddleware::prepareBody()尽可能补充默认Content-Type、默认Content-Length或Transfer-Encoding头以及Expect头封装为PrepareBodyMiddlewarecookies()的源码实现src/Middleware.php是一个很好的参考范式先校验$options[cookies]是否为CookieJarInterface否则抛InvalidArgumentException转发前调用$cookieJar-withCookieHeader($request)再在 Promise 兑现时调用$cookieJar-extractCookies($request, $response)。history()中间件常与Pool或异步并发场景配合用于事后审计与调试其容器元素结构为[request RequestInterface, response ?ResponseInterface, error mixed, options array]成功与失败分支见 src/Middleware.php。十一、总结与实践建议中间件 高阶函数牢记“工厂函数 → 接收 handler → 返回组合 handler”的三层结构这是理解与编写一切 Guzzle 扩展的基石。善用内置工厂mapRequest/mapResponse简化请求响应改写tap实现零侵入观察log搭配MessageFormatter模板输出结构化日志httpErrorsBodySummarizer控制错误消息体积retry配合 decider/delay 实现指数退避重试。用HandlerStack::create()起步它一次性装配好http_errors、allow_redirects、auth、cookies、prepare_body五个默认中间件需要自定义时再通过命名push/unshift/before/after/remove精确编排。注意敏感数据日志模板与错误摘要都可能携带凭据生产环境务必脱敏或使用受限模板。重试要保守优先只重试连接类错误与 429并配合合理的retries上限与退避延迟避免对下游造成压力。延伸阅读Handlers处理器 — 中间件所包装的底层处理器与默认栈执行顺序Request Options请求选项 —retries、delay、http_errors等选项的完整语义Testing Guzzle Clients测试 Guzzle 客户端 — 结合MockHandler与中间件进行测试赞分享后端【免费下载链接】guzzleGuzzle, an extensible PHP HTTP client项目地址https://gitcode.com/gh_mirrors/gu/guzzle点击查看免费下载相关推荐OpenCore Legacy Patcher五步让老旧Mac焕发新生的完整指南OpenCore Legacy Patcher五步让老旧Mac焕发新生的完整指南 当苹果宣布你的Mac不再支持最新的macOS系统时那种被技术抛弃的感觉令人操作系统固件驱动开发React Router Middleware 完全指南用框架/数据模式的前后置中间件统一鉴权、日志与错误处理React Router Middleware 完全指南用框架/数据模式的前后置中间件统一鉴权、日志与错误处理 Middleware中间件允许你在 Rea前端路由napi-rs错误码设计自定义错误类型与错误处理中间件napi rs错误码设计自定义错误类型与错误处理中间件 在Node.js与Rust混合编程中错误处理是保证应用稳定性的关键环节。napi rs框架通过类型化开发工具后端上一篇终极解决方案bRPC多版本兼容设计实战指南下一篇Perfetto GPU 计算内核分析用 NVIDIA Compute Workload Analysis 找出饱和流水线与指令配比瓶颈创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考