HTTP 客户端
简介
Laravel 提供了一个富有表现力、极简的 API,围绕 Guzzle HTTP 客户端,允许您快速发出出站 HTTP 请求以与其他 Web 应用程序通信。Laravel 对 Guzzle 的封装专注于其最常见的用例和出色的开发者体验。
发送请求
要发送请求,您可以使用 Http
外观模式提供的 head
、get
、post
、put
、patch
和 delete
方法。首先,让我们研究一下如何向另一个 URL 发出基本的 GET
请求
1use Illuminate\Support\Facades\Http;2 3$response = Http::get('http://example.com');
get
方法返回 Illuminate\Http\Client\Response
的实例,该实例提供了各种可用于检查响应的方法
1$response->body() : string; 2$response->json($key = null, $default = null) : mixed; 3$response->object() : object; 4$response->collect($key = null) : Illuminate\Support\Collection; 5$response->resource() : resource; 6$response->status() : int; 7$response->successful() : bool; 8$response->redirect(): bool; 9$response->failed() : bool;10$response->clientError() : bool;11$response->header($header) : string;12$response->headers() : array;
Illuminate\Http\Client\Response
对象还实现了 PHP ArrayAccess
接口,允许您直接在响应上访问 JSON 响应数据
1return Http::get('http://example.com/users/1')['name'];
除了上面列出的响应方法之外,以下方法可用于确定响应是否具有给定的状态代码
1$response->ok() : bool; // 200 OK 2$response->created() : bool; // 201 Created 3$response->accepted() : bool; // 202 Accepted 4$response->noContent() : bool; // 204 No Content 5$response->movedPermanently() : bool; // 301 Moved Permanently 6$response->found() : bool; // 302 Found 7$response->badRequest() : bool; // 400 Bad Request 8$response->unauthorized() : bool; // 401 Unauthorized 9$response->paymentRequired() : bool; // 402 Payment Required10$response->forbidden() : bool; // 403 Forbidden11$response->notFound() : bool; // 404 Not Found12$response->requestTimeout() : bool; // 408 Request Timeout13$response->conflict() : bool; // 409 Conflict14$response->unprocessableEntity() : bool; // 422 Unprocessable Entity15$response->tooManyRequests() : bool; // 429 Too Many Requests16$response->serverError() : bool; // 500 Internal Server Error
URI 模板
HTTP 客户端还允许您使用 URI 模板规范 构建请求 URL。要定义可以由 URI 模板扩展的 URL 参数,您可以使用 withUrlParameters
方法
1Http::withUrlParameters([2 'endpoint' => 'https://laravel.net.cn',3 'page' => 'docs',4 'version' => '11.x',5 'topic' => 'validation',6])->get('{+endpoint}/{page}/{version}/{topic}');
倾印请求
如果您想在发送传出请求实例之前倾印它并终止脚本的执行,您可以将 dd
方法添加到请求定义的开头
1return Http::dd()->get('http://example.com');
请求数据
当然,在发出 POST
、PUT
和 PATCH
请求时,通常会随请求发送其他数据,因此这些方法接受数据数组作为其第二个参数。默认情况下,数据将使用 application/json
内容类型发送
1use Illuminate\Support\Facades\Http;2 3$response = Http::post('http://example.com/users', [4 'name' => 'Steve',5 'role' => 'Network Administrator',6]);
GET 请求查询参数
在发出 GET
请求时,您可以将查询字符串直接附加到 URL,也可以将键/值对数组作为 get
方法的第二个参数传递
1$response = Http::get('http://example.com/users', [2 'name' => 'Taylor',3 'page' => 1,4]);
或者,可以使用 withQueryParameters
方法
1Http::retry(3, 100)->withQueryParameters([2 'name' => 'Taylor',3 'page' => 1,4])->get('http://example.com/users')
发送表单 URL 编码的请求
如果您想使用 application/x-www-form-urlencoded
内容类型发送数据,您应该在发出请求之前调用 asForm
方法
1$response = Http::asForm()->post('http://example.com/users', [2 'name' => 'Sara',3 'role' => 'Privacy Consultant',4]);
发送原始请求体
如果您想在发出请求时提供原始请求体,可以使用 withBody
方法。内容类型可以通过该方法的第二个参数提供
1$response = Http::withBody(2 base64_encode($photo), 'image/jpeg'3)->post('http://example.com/photo');
多部分请求
如果您想将文件作为多部分请求发送,您应该在发出请求之前调用 attach
方法。此方法接受文件名及其内容。如果需要,您可以提供第三个参数,该参数将被视为文件的文件名,而第四个参数可用于提供与文件关联的标头
1$response = Http::attach(2 'attachment', file_get_contents('photo.jpg'), 'photo.jpg', ['Content-Type' => 'image/jpeg']3)->post('http://example.com/attachments');
您可以传递流资源,而不是传递文件的原始内容
1$photo = fopen('photo.jpg', 'r');2 3$response = Http::attach(4 'attachment', $photo, 'photo.jpg'5)->post('http://example.com/attachments');
标头
可以使用 withHeaders
方法将标头添加到请求。此 withHeaders
方法接受键/值对数组
1$response = Http::withHeaders([2 'X-First' => 'foo',3 'X-Second' => 'bar'4])->post('http://example.com/users', [5 'name' => 'Taylor',6]);
您可以使用 accept
方法指定您的应用程序期望在响应您的请求时返回的内容类型
1$response = Http::accept('application/json')->get('http://example.com/users');
为了方便起见,您可以使用 acceptJson
方法快速指定您的应用程序期望在响应您的请求时返回 application/json
内容类型
1$response = Http::acceptJson()->get('http://example.com/users');
withHeaders
方法将新标头合并到请求的现有标头中。如果需要,您可以使用 replaceHeaders
方法完全替换所有标头
1$response = Http::withHeaders([2 'X-Original' => 'foo',3])->replaceHeaders([4 'X-Replacement' => 'bar',5])->post('http://example.com/users', [6 'name' => 'Taylor',7]);
身份验证
您可以使用 withBasicAuth
和 withDigestAuth
方法分别指定基本和摘要身份验证凭据
1// Basic authentication...3 4// Digest authentication...
Bearer 令牌
如果您想快速将 Bearer 令牌添加到请求的 Authorization
标头,您可以使用 withToken
方法
1$response = Http::withToken('token')->post(/* ... */);
超时
timeout
方法可用于指定等待响应的最长秒数。默认情况下,HTTP 客户端将在 30 秒后超时
1$response = Http::timeout(3)->get(/* ... */);
如果超过给定的超时时间,将抛出 Illuminate\Http\Client\ConnectionException
的实例。
您可以使用 connectTimeout
方法指定尝试连接到服务器时等待的最长秒数
1$response = Http::connectTimeout(3)->get(/* ... */);
重试
如果您希望 HTTP 客户端在发生客户端或服务器错误时自动重试请求,您可以使用 retry
方法。retry
方法接受请求应尝试的最大次数以及 Laravel 在尝试之间应等待的毫秒数
1$response = Http::retry(3, 100)->post(/* ... */);
如果您想手动计算尝试之间休眠的毫秒数,您可以将闭包作为 retry
方法的第二个参数传递
1use Exception;2 3$response = Http::retry(3, function (int $attempt, Exception $exception) {4 return $attempt * 100;5})->post(/* ... */);
为了方便起见,您还可以提供一个数组作为 retry
方法的第一个参数。此数组将用于确定后续尝试之间休眠的毫秒数
1$response = Http::retry([100, 200])->post(/* ... */);
如果需要,您可以将第三个参数传递给 retry
方法。第三个参数应该是一个可调用对象,用于确定是否应实际尝试重试。例如,您可能希望仅在初始请求遇到 ConnectionException
时重试请求
1use Exception;2use Illuminate\Http\Client\PendingRequest;3 4$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) {5 return $exception instanceof ConnectionException;6})->post(/* ... */);
如果请求尝试失败,您可能希望在进行新的尝试之前更改请求。您可以通过修改提供给您提供给 retry
方法的可调用对象的请求参数来实现此目的。例如,如果第一次尝试返回身份验证错误,您可能希望使用新的授权令牌重试请求
1use Exception; 2use Illuminate\Http\Client\PendingRequest; 3use Illuminate\Http\Client\RequestException; 4 5$response = Http::withToken($this->getToken())->retry(2, 0, function (Exception $exception, PendingRequest $request) { 6 if (! $exception instanceof RequestException || $exception->response->status() !== 401) { 7 return false; 8 } 9 10 $request->withToken($this->getNewToken());11 12 return true;13})->post(/* ... */);
如果所有请求都失败,将抛出 Illuminate\Http\Client\RequestException
的实例。如果您想禁用此行为,您可以提供一个 throw
参数,其值为 false
。禁用后,在所有重试尝试后,将返回客户端收到的最后一个响应
1$response = Http::retry(3, 100, throw: false)->post(/* ... */);
如果所有请求都因连接问题而失败,即使 throw
参数设置为 false
,仍然会抛出 Illuminate\Http\Client\ConnectionException
。
错误处理
与 Guzzle 的默认行为不同,Laravel 的 HTTP 客户端封装器不会在客户端或服务器错误(服务器返回的 400
和 500
级别响应)时抛出异常。您可以使用 successful
、clientError
或 serverError
方法确定是否返回了其中一个错误
1// Determine if the status code is >= 200 and < 300... 2$response->successful(); 3 4// Determine if the status code is >= 400... 5$response->failed(); 6 7// Determine if the response has a 400 level status code... 8$response->clientError(); 9 10// Determine if the response has a 500 level status code...11$response->serverError();12 13// Immediately execute the given callback if there was a client or server error...14$response->onError(callable $callback);
抛出异常
如果您有一个响应实例,并且希望在响应状态代码指示客户端或服务器错误时抛出 Illuminate\Http\Client\RequestException
的实例,您可以使用 throw
或 throwIf
方法
1use Illuminate\Http\Client\Response; 2 3$response = Http::post(/* ... */); 4 5// Throw an exception if a client or server error occurred... 6$response->throw(); 7 8// Throw an exception if an error occurred and the given condition is true... 9$response->throwIf($condition);10 11// Throw an exception if an error occurred and the given closure resolves to true...12$response->throwIf(fn (Response $response) => true);13 14// Throw an exception if an error occurred and the given condition is false...15$response->throwUnless($condition);16 17// Throw an exception if an error occurred and the given closure resolves to false...18$response->throwUnless(fn (Response $response) => false);19 20// Throw an exception if the response has a specific status code...21$response->throwIfStatus(403);22 23// Throw an exception unless the response has a specific status code...24$response->throwUnlessStatus(200);25 26return $response['user']['id'];
Illuminate\Http\Client\RequestException
实例具有公共 $response
属性,该属性允许您检查返回的响应。
如果未发生错误,throw
方法将返回响应实例,允许您将其他操作链接到 throw
方法
1return Http::post(/* ... */)->throw()->json();
如果您想在抛出异常之前执行一些额外的逻辑,您可以将闭包传递给 throw
方法。异常将在调用闭包后自动抛出,因此您无需从闭包中重新抛出异常
1use Illuminate\Http\Client\Response;2use Illuminate\Http\Client\RequestException;3 4return Http::post(/* ... */)->throw(function (Response $response, RequestException $e) {5 // ...6})->json();
默认情况下,当记录或报告时,RequestException
消息会被截断为 120 个字符。要自定义或禁用此行为,您可以在 bootstrap/app.php
文件中配置应用程序的异常处理行为时使用 truncateRequestExceptionsAt
和 dontTruncateRequestExceptions
方法
1->withExceptions(function (Exceptions $exceptions) {2 // Truncate request exception messages to 240 characters...3 $exceptions->truncateRequestExceptionsAt(240);4 5 // Disable request exception message truncation...6 $exceptions->dontTruncateRequestExceptions();7})
Guzzle 中间件
由于 Laravel 的 HTTP 客户端由 Guzzle 提供支持,因此您可以利用 Guzzle 中间件 来操作传出请求或检查传入响应。要操作传出请求,请通过 withRequestMiddleware
方法注册 Guzzle 中间件
1use Illuminate\Support\Facades\Http;2use Psr\Http\Message\RequestInterface;3 4$response = Http::withRequestMiddleware(5 function (RequestInterface $request) {6 return $request->withHeader('X-Example', 'Value');7 }8)->get('http://example.com');
同样,您可以通过 withResponseMiddleware
方法注册中间件来检查传入的 HTTP 响应
1use Illuminate\Support\Facades\Http; 2use Psr\Http\Message\ResponseInterface; 3 4$response = Http::withResponseMiddleware( 5 function (ResponseInterface $response) { 6 $header = $response->getHeader('X-Example'); 7 8 // ... 9 10 return $response;11 }12)->get('http://example.com');
全局中间件
有时,您可能希望注册一个应用于每个传出请求和传入响应的中间件。要实现此目的,您可以使用 globalRequestMiddleware
和 globalResponseMiddleware
方法。通常,这些方法应在应用程序的 AppServiceProvider
的 boot
方法中调用
1use Illuminate\Support\Facades\Http;2 3Http::globalRequestMiddleware(fn ($request) => $request->withHeader(4 'User-Agent', 'Example Application/1.0'5));6 7Http::globalResponseMiddleware(fn ($response) => $response->withHeader(8 'X-Finished-At', now()->toDateTimeString()9));
Guzzle 选项
您可以使用 withOptions
方法为传出请求指定其他 Guzzle 请求选项。withOptions
方法接受键/值对数组
1$response = Http::withOptions([2 'debug' => true,3])->get('http://example.com/users');
全局选项
要为每个传出请求配置默认选项,您可以使用 globalOptions
方法。通常,此方法应从应用程序的 AppServiceProvider
的 boot
方法中调用
1use Illuminate\Support\Facades\Http; 2 3/** 4 * Bootstrap any application services. 5 */ 6public function boot(): void 7{ 8 Http::globalOptions([ 9 'allow_redirects' => false,10 ]);11}
并发请求
有时,您可能希望并发发出多个 HTTP 请求。换句话说,您希望同时调度多个请求,而不是按顺序发出请求。当与缓慢的 HTTP API 交互时,这可以显着提高性能。
值得庆幸的是,您可以使用 pool
方法来实现此目的。pool
方法接受一个闭包,该闭包接收 Illuminate\Http\Client\Pool
实例,允许您轻松地将请求添加到请求池以进行调度
1use Illuminate\Http\Client\Pool; 2use Illuminate\Support\Facades\Http; 3 4$responses = Http::pool(fn (Pool $pool) => [ 5 $pool->get('https://127.0.0.1/first'), 6 $pool->get('https://127.0.0.1/second'), 7 $pool->get('https://127.0.0.1/third'), 8]); 9 10return $responses[0]->ok() &&11 $responses[1]->ok() &&12 $responses[2]->ok();
如您所见,每个响应实例都可以根据添加到池中的顺序进行访问。如果您愿意,可以使用 as
方法命名请求,这允许您按名称访问相应的响应
1use Illuminate\Http\Client\Pool; 2use Illuminate\Support\Facades\Http; 3 4$responses = Http::pool(fn (Pool $pool) => [ 5 $pool->as('first')->get('https://127.0.0.1/first'), 6 $pool->as('second')->get('https://127.0.0.1/second'), 7 $pool->as('third')->get('https://127.0.0.1/third'), 8]); 9 10return $responses['first']->ok();
自定义并发请求
pool
方法不能与其他 HTTP 客户端方法(如 withHeaders
或 middleware
方法)链接。如果您想将自定义标头或中间件应用于池化请求,则应在池中的每个请求上配置这些选项
1use Illuminate\Http\Client\Pool; 2use Illuminate\Support\Facades\Http; 3 4$headers = [ 5 'X-Example' => 'example', 6]; 7 8$responses = Http::pool(fn (Pool $pool) => [ 9 $pool->withHeaders($headers)->get('http://laravel.test/test'),10 $pool->withHeaders($headers)->get('http://laravel.test/test'),11 $pool->withHeaders($headers)->get('http://laravel.test/test'),12]);
宏
Laravel HTTP 客户端允许您定义“宏”,宏可以作为流畅、富有表现力的机制,用于在与整个应用程序的服务交互时配置常见的请求路径和标头。要开始使用,您可以在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中定义宏
1use Illuminate\Support\Facades\Http; 2 3/** 4 * Bootstrap any application services. 5 */ 6public function boot(): void 7{ 8 Http::macro('github', function () { 9 return Http::withHeaders([10 'X-Example' => 'example',11 ])->baseUrl('https://github.com');12 });13}
配置宏后,您可以从应用程序中的任何位置调用它,以创建具有指定配置的待处理请求
1$response = Http::github()->get('/');
测试
许多 Laravel 服务都提供了帮助您轻松且富有表现力地编写测试的功能,Laravel 的 HTTP 客户端也不例外。Http
外观模式的 fake
方法允许您指示 HTTP 客户端在发出请求时返回存根/虚拟响应。
伪造响应
例如,要指示 HTTP 客户端为每个请求返回空的 200
状态代码响应,您可以调用不带参数的 fake
方法
1use Illuminate\Support\Facades\Http;2 3Http::fake();4 5$response = Http::post(/* ... */);
伪造特定 URL
或者,您可以将数组传递给 fake
方法。该数组的键应表示您希望伪造的 URL 模式及其关联的响应。*
字符可以用作通配符。对未伪造的 URL 发出的任何请求都将实际执行。您可以使用 Http
外观模式的 response
方法为这些端点构造存根/伪造响应
1Http::fake([2 // Stub a JSON response for GitHub endpoints...3 'github.com/*' => Http::response(['foo' => 'bar'], 200, $headers),4 5 // Stub a string response for Google endpoints...6 'google.com/*' => Http::response('Hello World', 200, $headers),7]);
如果您想指定一个回退 URL 模式,该模式将存根所有不匹配的 URL,您可以使用单个 *
字符
1Http::fake([2 // Stub a JSON response for GitHub endpoints...3 'github.com/*' => Http::response(['foo' => 'bar'], 200, ['Headers']),4 5 // Stub a string response for all other endpoints...6 '*' => Http::response('Hello World', 200, ['Headers']),7]);
为了方便起见,可以通过提供字符串、数组或整数作为响应来生成简单的字符串、JSON 和空响应
1Http::fake([2 'google.com/*' => 'Hello World',3 'github.com/*' => ['foo' => 'bar'],4 'chatgpt.com/*' => 200,5]);
伪造连接异常
有时,您可能需要测试您的应用程序在 HTTP 客户端尝试发出请求时遇到 Illuminate\Http\Client\ConnectionException
时的行为。您可以使用 failedConnection
方法指示 HTTP 客户端抛出连接异常
1Http::fake([2 'github.com/*' => Http::failedConnection(),3]);
伪造响应序列
有时,您可能需要指定单个 URL 应按特定顺序返回一系列伪造响应。您可以使用 Http::sequence
方法构建响应来实现此目的
1Http::fake([2 // Stub a series of responses for GitHub endpoints...3 'github.com/*' => Http::sequence()4 ->push('Hello World', 200)5 ->push(['foo' => 'bar'], 200)6 ->pushStatus(404),7]);
当响应序列中的所有响应都已使用完时,任何进一步的请求都将导致响应序列抛出异常。如果您想指定一个在序列为空时应返回的默认响应,您可以使用 whenEmpty
方法
1Http::fake([2 // Stub a series of responses for GitHub endpoints...3 'github.com/*' => Http::sequence()4 ->push('Hello World', 200)5 ->push(['foo' => 'bar'], 200)6 ->whenEmpty(Http::response()),7]);
如果您想伪造一系列响应,但不需要指定应伪造的特定 URL 模式,您可以使用 Http::fakeSequence
方法
1Http::fakeSequence()2 ->push('Hello World', 200)3 ->whenEmpty(Http::response());
伪造回调
如果您需要更复杂的逻辑来确定要为某些端点返回哪些响应,您可以将闭包传递给 fake
方法。此闭包将接收 Illuminate\Http\Client\Request
的实例,并应返回响应实例。在您的闭包中,您可以执行任何必要的逻辑来确定要返回的响应类型
1use Illuminate\Http\Client\Request;2 3Http::fake(function (Request $request) {4 return Http::response('Hello World', 200);5});
防止意外请求
如果您想确保通过 HTTP 客户端发送的所有请求都在您的单个测试或完整测试套件中被伪造,您可以调用 preventStrayRequests
方法。调用此方法后,任何没有相应伪造响应的请求都将抛出异常,而不是发出实际的 HTTP 请求
1use Illuminate\Support\Facades\Http; 2 3Http::preventStrayRequests(); 4 5Http::fake([ 6 'github.com/*' => Http::response('ok'), 7]); 8 9// An "ok" response is returned...10Http::get('https://github.com/laravel/framework');11 12// An exception is thrown...13Http::get('https://laravel.net.cn');
检查请求
伪造响应时,您可能偶尔希望检查客户端收到的请求,以确保您的应用程序发送了正确的数据或标头。您可以通过在调用 Http::fake
后调用 Http::assertSent
方法来实现此目的。
assertSent
方法接受一个闭包,该闭包将接收 Illuminate\Http\Client\Request
实例,并应返回一个布尔值,指示请求是否符合您的期望。为了使测试通过,必须已发出至少一个符合给定期望的请求
1use Illuminate\Http\Client\Request; 2use Illuminate\Support\Facades\Http; 3 4Http::fake(); 5 6Http::withHeaders([ 7 'X-First' => 'foo', 8])->post('http://example.com/users', [ 9 'name' => 'Taylor',10 'role' => 'Developer',11]);12 13Http::assertSent(function (Request $request) {14 return $request->hasHeader('X-First', 'foo') &&15 $request->url() == 'http://example.com/users' &&16 $request['name'] == 'Taylor' &&17 $request['role'] == 'Developer';18});
如果需要,您可以使用 assertNotSent
方法断言未发送特定请求
1use Illuminate\Http\Client\Request; 2use Illuminate\Support\Facades\Http; 3 4Http::fake(); 5 6Http::post('http://example.com/users', [ 7 'name' => 'Taylor', 8 'role' => 'Developer', 9]);10 11Http::assertNotSent(function (Request $request) {12 return $request->url() === 'http://example.com/posts';13});
您可以使用 assertSentCount
方法断言在测试期间“发送”了多少个请求
1Http::fake();2 3Http::assertSentCount(5);
或者,您可以使用 assertNothingSent
方法断言在测试期间未发送任何请求
1Http::fake();2 3Http::assertNothingSent();
记录请求/响应
您可以使用 recorded
方法收集所有请求及其相应的响应。recorded
方法返回一个数组集合,其中包含 Illuminate\Http\Client\Request
和 Illuminate\Http\Client\Response
的实例
1Http::fake([ 2 'https://laravel.net.cn' => Http::response(status: 500), 3 'https://nova.laravel.net.cn/' => Http::response(), 4]); 5 6Http::get('https://laravel.net.cn'); 7Http::get('https://nova.laravel.net.cn/'); 8 9$recorded = Http::recorded();10 11[$request, $response] = $recorded[0];
此外,recorded
方法接受一个闭包,该闭包将接收 Illuminate\Http\Client\Request
和 Illuminate\Http\Client\Response
的实例,并且可以用于根据您的期望过滤请求/响应对
1use Illuminate\Http\Client\Request; 2use Illuminate\Http\Client\Response; 3 4Http::fake([ 5 'https://laravel.net.cn' => Http::response(status: 500), 6 'https://nova.laravel.net.cn/' => Http::response(), 7]); 8 9Http::get('https://laravel.net.cn');10Http::get('https://nova.laravel.net.cn/');11 12$recorded = Http::recorded(function (Request $request, Response $response) {13 return $request->url() !== 'https://laravel.net.cn' &&14 $response->successful();15});
事件
Laravel 在发送 HTTP 请求的过程中触发三个事件。RequestSending
事件在请求发送之前触发,而 ResponseReceived
事件在收到给定请求的响应后触发。如果未收到给定请求的响应,则会触发 ConnectionFailed
事件。
RequestSending
和 ConnectionFailed
事件都包含一个公共 $request
属性,您可以使用该属性检查 Illuminate\Http\Client\Request
实例。同样,ResponseReceived
事件包含一个 $request
属性以及一个 $response
属性,您可以使用它们检查 Illuminate\Http\Client\Response
实例。您可以在您的应用程序中为这些事件创建事件监听器
1use Illuminate\Http\Client\Events\RequestSending; 2 3class LogRequest 4{ 5 /** 6 * Handle the given event. 7 */ 8 public function handle(RequestSending $event): void 9 {10 // $event->request ...11 }12}