如何在Laravel HTTP客户端的ResponseMiddleware中关联请求与响应?
Laravel HTTP客户端自定义日志:关联请求与响应日志
需求背景
已通过Laravel HTTP客户端的withRequestMiddleware实现每次API调用(含重试)的请求日志记录,请求携带唯一X-RequestId标识实现分组日志。现在需要在withResponseMiddleware中获取对应的requestId和attempt,将响应信息与请求日志关联更新。
解决方案
核心思路是在请求中间件中将identifier和attempt存入请求的自定义属性,后续在响应中间件中通过响应对象获取关联的请求实例,进而取出这些属性完成日志更新。
步骤1:修改请求中间件,存储关键标识到请求属性
在请求中间件中,除插入请求日志外,额外将identifier和attempt存入请求的自定义属性:
$httpClient = Http::withBasicAuth($this->apiKey, $this->apiSecret) ->retry(3, 100, null, false) ->withRequestMiddleware(function (RequestInterface $request) { $identifier = $request->getHeaderLine('X-RequestId'); $attempt = $request->getAttribute('retry_attempts', 1); // 存入请求自定义属性,供响应中间件调用 $request->setAttribute('log_identifier', $identifier); $request->setAttribute('log_attempt', $attempt); $authMethod = $request->getHeaderLine('Authorization') ?? null; $authMethod = is_string($authMethod) ? preg_replace("/\s*(\w+)\s+.*/", "$1", $authMethod) : null; DB::table('api_call_log')->insert([ 'identifier' => $identifier, 'attempt' => $attempt, 'method' => $request->getMethod(), 'url' => $request->getUri(), 'request_auth_method' => $authMethod, 'headers' => serialize($request->getHeaders()), 'body' => serialize($request->getBody()), 'request_start_unix' => now()->unix(), ]); return $request; }) ->withResponseMiddleware(function (ResponseInterface $response) { // 从响应关联的请求实例中取出标识 $request = $response->getRequest(); $identifier = $request->getAttribute('log_identifier'); $attempt = $request->getAttribute('log_attempt'); // 更新响应日志数据 DB::table('api_call_log') ->where(['identifier' => $identifier, 'attempt' => $attempt]) ->update([ 'request_end_unix' => now()->unix(), 'response_code' => $response->getStatusCode(), 'response_body' => serialize($response->getBody()), 'response_headers' => serialize($response->getHeaders()), ]); });
步骤2:请求发起逻辑(保持不变)
发起请求时仍需携带X-RequestId标识:
$requestId = Str::uuid()->toString(); $endpoint = $this->apiUrl.'/myendpoint'; $response = $this->httpClient->withHeaders(['X-RequestId' => $requestId]) ->put($endpoint, ['name' => "Hello"]);
数据表迁移(已提供)
确保日志表结构支持请求与响应字段的存储:
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. */ public function up(): void { Schema::dropIfExists('api_call_log'); Schema::create('api_call_log', function (Blueprint $table) { $table->string('identifier', 32); $table->integer('attempt')->default(1)->unsigned(); $table->primary(['identifier','attempt']); $table->string('method', 32); $table->string('url', 32); $table->string('request_auth_method', 32)->nullable(); $table->binary('headers')->nullable(); $table->binary('body')->nullable(); $table->integer('request_start_unix')->unsigned(); $table->integer('request_end_unix')->unsigned()->nullable(); $table->smallInteger('response_code')->unsigned()->nullable(); $table->binary('response_body')->nullable(); $table->binary('response_headers')->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('api_call_log'); } };
关键说明
- Guzzle的
ResponseInterface提供getRequest()方法,可获取当前响应对应的请求实例 - 通过
setAttribute()和getAttribute()操作请求的自定义属性,安全传递上下文数据 - 利用复合主键
identifier+attempt确保每次重试请求的日志能被准确更新
内容的提问来源于stack exchange,提问作者Dimitrios Desyllas
相关产品推荐
相关产品推荐

