Laravel测试内部HTTP请求返回500错误及请求卡死问题如何解决?
根因分析
- 本地需要启动两个
artisan serve的核心原因是PHP内置服务器为单进程单线程模型,同一时间只能处理一个请求:你在第一个请求里通过Guzzle向同个服务发起第二个请求,进程被第一个请求占着,没法响应第二个请求,就会无限阻塞,必须启动第二个进程处理Oauth相关请求才能正常返回。 - 测试时报500的原因和你猜测的一致:phpunit运行时会加载phpunit.xml里的环境变量,默认没有配置正确的
OAUTH_DOMAIN,就算配置了,测试环境也没有运行对应的Oauth服务,Guzzle请求失败就会走到catch分支返回500。
最优解决方案(推荐):移除内部HTTP请求,直接调用Passport内置方法生成令牌
你不需要通过HTTP请求调用自身的Oauth接口,Laravel Passport已经提供了password授权类型的令牌生成方法,直接调用即可,完全规避跨进程请求的问题,性能也更高。
修改你的TokenController代码如下:
<?php use App\Http\Controllers\Controller; use App\Http\Requests\Api\TokenRequest; use App\Repositories\ExceptionRepository; use Illuminate\Http\JsonResponse; use Laravel\Passport\Http\Controllers\ConvertsPsrResponses; use League\OAuth2\Server\AuthorizationServer; use Nyholm\Psr7\Request as PsrRequest; class TokenController extends Controller { use ConvertsPsrResponses; public function __construct(protected AuthorizationServer $server) { } public function __invoke(TokenRequest $request) : JsonResponse { try { // 构造PSR请求,直接调用授权服务生成令牌,不需要发HTTP请求 $psrRequest = (new PsrRequest('POST', 'oauth/token')) ->withParsedBody([ 'grant_type' => 'password', 'client_id' => env('PASSPORT_PASSWORD_CLIENT_ID'), 'client_secret' => env('PASSPORT_PASSWORD_CLIENT_SECRET'), 'username' => $request->input('email'), 'password' => $request->input('password'), 'scope' => '*', ]); $response = $this->server->respondToAccessTokenRequest($psrRequest, new \Nyholm\Psr7\Response()); $token = json_decode((string) $response->getBody(), true); return response()->json([ 'message' => 'Token retrieved successfully.', 'token' => $token, ]); } catch (Throwable $throwable) { ExceptionRepository::emergency('Something went wrong.', $throwable); return response()->json([ 'message' => 'Token can\' be retrieved.', 'error' => 'Internal server error' ], 500); } } }
修改后本地不需要启动两个artisan serve即可正常运行,测试时也不会出现请求失败的问题。
如果你必须保留原有Guzzle请求的写法
本地调试优化
不要使用artisan serve作为运行环境,改用Laravel Valet、Laravel Herd、Docker等多进程运行环境,即可支持同时处理多个请求,不需要启动多个服务实例。
测试环境修复
- 首先在
phpunit.xml中配置正确的环境变量,或者在测试用例中用putenv临时设置变量 - 将原生Guzzle调用改为Laravel封装的
Http门面,方便测试时Mock请求:
修改控制器里的请求部分:
$response = Http::asForm()->post(env('OAUTH_DOMAIN', 'https://oauth.application.com') . '/oauth/token', [ 'grant_type' => 'password', 'client_id' => env('PASSPORT_PASSWORD_CLIENT_ID'), 'client_secret' => env('PASSPORT_PASSWORD_CLIENT_SECRET'), 'username' => $request->input('email'), 'password' => $request->input('password'), 'scope' => '*', ]);
然后在测试用例中添加Mock逻辑,模拟接口返回:
<?php use App\Models\User; use Illuminate\Support\Facades\Http; public function test_token() { // Mock Oauth接口返回 Http::fake([ '*oauth/token' => Http::response([ 'access_token' => 'fake-token', 'token_type' => 'Bearer', 'expires_in' => 7200, ], 200) ]); $user = User::factory()->create(); $response = $this->json('POST', '/api/token', [ 'email' => $user->email, 'password' => 'password', ]); // 正常返回应该断言200,而不是500 $response->assertStatus(200)->assertJsonPath('token.access_token', 'fake-token'); }
可选优化
将所有代码中直接调用env的逻辑移到config配置文件中,避免执行配置缓存后env返回null的问题。
内容的提问来源于stack exchange,提问作者Ian Antonio Torres Gonzalez
相关产品推荐
相关产品推荐

