You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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等多进程运行环境,即可支持同时处理多个请求,不需要启动多个服务实例。

测试环境修复

  1. 首先在phpunit.xml中配置正确的环境变量,或者在测试用例中用putenv临时设置变量
  2. 将原生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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 09:45:05