首页
/ Coolify 中的 Laravel HTTP Client 最佳实践:超时、重试、并发请求与测试 Faking 实战

Coolify 中的 Laravel HTTP Client 最佳实践:超时、重试、并发请求与测试 Faking 实战

2026-09-04 17:07:38作者:裴麒琰

Coolify 是一个基于 Laravel 的自托管 PaaS,其自身大量依赖 Laravel HTTP Client 与 GitHub Releases、BunnyCDN、各云厂商 API、Stripe 等外部服务通信。本文基于仓库内置的规范文档 http-client.md(位于 laravel-best-practices 技能包中,在 SKILL.md 中被列为第 11 条规则),系统讲解五条 HTTP Client 核心实践——显式超时、带退避的重试、显式错误处理、请求池并发、测试 Faking——并结合 Coolify 源码给出可对照的真实调用链,帮助你在自己项目中落地一套可靠的对外 API 调用模式。

一、为什么必须设置显式超时(timeout 与 connectTimeout)

规范文档的第一条规则:默认超时是 30 秒,对大多数 API 调用来说太长,应当总是显式设置 timeoutconnectTimeout,让故障快速暴露(fail fast)。

反例与正例对照:

// 错误:依赖 30 秒默认超时
$response = Http::get('https://api.example.com/users');

// 正确:显式设置超时
$response = Http::timeout(5)
    ->connectTimeout(3)
    ->get('https://api.example.com/users');

文档还给出了一个进阶建议:对特定服务封装 HTTP 宏(macro),把超时、基址、鉴权等参数集中管理:

Http::macro('github', function () {
    return Http::baseUrl('https://api.github.com')
        ->timeout(10)
        ->connectTimeout(3)
        ->withToken(config('services.github.token'));
});

$response = Http::github()->get('/repos/laravel/framework');

Coolify 中的真实落地

1. 版本检查场景:短超时 + 快速失败。 UpdateCoolify 负责从 CDN 拉取最新版本号,使用 10 秒超时,且超时失败后会回退到本地缓存的版本号(get_latest_version_of_coolify()),并校验缓存版本不得小于当前运行版本——这是一个「超时 → 降级到缓存 → 版本合法性校验」的完整防御式写法:

$response = Http::retry(3, 1000)->timeout(10)
    ->get(config('constants.coolify.versions_url'));

其中 versions_urlconfig/constants.php 中定义为:

'versions_url' => env('VERSIONS_URL', env('CDN_URL', 'https://cdn.coollabs.io').'/coolify/versions.json'),

同一模式还出现在 CheckForUpdatesJobCheckHelperImageJobPullChangelog 中,全部使用 retry(3, 1000) 组合。

2. 长耗时场景:超时按语义放大。 服务器迁移是典型的长请求,ServerTransferMigrator 将超时显式设为 120 秒,说明超时应根据接口语义(读版本 JSON vs 传输大文件)分别设定,而不是全局一刀切。

3. Webhook 场景:更短的超时。 API 的 webhook 转发在 OtherController 中只给 5 秒:Http::timeout(5)->post($webhook_url, [...]),因为 webhook 是同步附加动作,不能拖慢主请求。

4. 服务级宏:Coolify 的 GitHub / GitLab 客户端。 AppServiceProvider 中注册了两个宏,思路与文档示例一致,但更贴合实际(支持自托管实例、可选 token):

Http::macro('GitHub', function (string $api_url, ?string $github_access_token = null) {
    if ($github_access_token) {
        return Http::withHeaders([
            'X-GitHub-Api-Version' => '2022-11-28',
            'Accept' => 'application/vnd.github.v3+json',
            'Authorization' => "Bearer $github_access_token",
        ])->baseUrl($api_url);
    }
    return Http::withHeaders([
        'Accept' => 'application/vnd.github.v3+json',
    ])->baseUrl($api_url);
});

从源码结构看,Coolify 的宏没有内置 timeout,而是把「连接参数(基址、鉴权头)」与「调用点超时」解耦——超时放在每个具体调用处(如版本检查的 10 秒),这样同一服务在不同场景下可以有不同的超时策略。

二、用 retry() + 退避应对外部 API 的瞬时故障

规范文档第二条:外部 API 存在瞬时故障,应使用 retry() 配递增延迟,而不是让第一次失败直接抛错:

// 错误:一次失败即抛错
$response = Http::post('https://api.stripe.com/v1/charges', $data);
if ($response->failed()) {
    throw new PaymentFailedException('Charge failed');
}

// 正确:退避重试
$response = Http::retry([100, 500, 1000])
    ->timeout(10)
    ->post('https://api.stripe.com/v1/charges', $data);

文档同时强调只应对特定错误重试,并给出带异常判定回调的写法:

$response = Http::retry(3, 100, function (Throwable $exception, PendingRequest $request) {
    return $exception instanceof ConnectionException
        || ($exception instanceof RequestException && $exception->response->serverError());
})->post('https://api.example.com/data');

也就是说:连接异常(ConnectionException)和 5xx(serverError())值得重试;4xx 客户端错误重试毫无意义,只会放大压力。

Coolify 的重试模式

Coolify 中检索不到对 Stripe 的裸 Http::post(支付走官方 Stripe SDK),但版本检查、变更日志、模板拉取等「读外部数据」场景统一采用 retry(3, 1000)(3 次重试、固定 1000ms 延迟):

  • CheckForUpdatesJobHttp::retry(3, 1000)->get(config('constants.coolify.versions_url'))
  • InitHttp::retry(3, 1000, throw: false) —— 通过 throw: false 关闭抛异常,转而手动检查响应状态,适合「失败可容忍」的启动流程
  • PullTemplatesFromCDN:同样是 retry(3, 1000, throw: false) 模式

这些场景的共同特点是:请求方是队列 Job 或 artisan 命令,本身具备失败重跑能力,HTTP 层的 3 次退避重试相当于在任务粒度之上再加一层细粒度容错。

三、显式处理 4xx/5xx:throw() 与优雅降级

规范文档第三条:Laravel HTTP Client 默认不会因 4xx/5xx 抛异常,必须检查状态码或显式调用 throw()

// 错误:直接解析,可能是错误响应体
$response = Http::get('https://api.example.com/users/1');
$user = $response->json();

// 正确:throw() 显式抛错
$response = Http::timeout(5)
    ->get('https://api.example.com/users/1')
    ->throw();
$user = $response->json();

文档还给出「优雅降级」版本,按状态分支处理,把 404 语义化为 null

$response = Http::get('https://api.example.com/users/1');

if ($response->successful()) {
    return $response->json();
}

if ($response->notFound()) {
    return null;
}

$response->throw();

Coolify 中的「成功/失败」分支写法

UpdateCoolify 是这一模式的典型实现:不依赖 throw(),而是先判断 $response->successful(),成功则解析 JSON;失败或抛异常时降级到缓存版本,并用 version_compare() 校验缓存版本不能落后于当前运行版本,避免基于脏数据做出错误的自动更新决策。日志上区分了 Log::error(缓存也损坏、不可恢复)与 Log::warning(CDN 不可用但缓存可用)两个级别,便于告警分级。

这个例子印证了文档的意图:successful() / notFound() / throw() 三件套应组合使用,让「哪些状态是业务内预期、哪些必须上抛」显式地写在调用点。

四、用 Http::pool() 并发多个独立请求

规范文档第四条:多个相互独立的 API 调用应使用 Http::pool() 并发执行,而非串行等待:

// 错误:串行三次往返
$users = Http::get('https://api.example.com/users')->json();
$posts = Http::get('https://api.example.com/posts')->json();
$comments = Http::get('https://api.example.com/comments')->json();

// 正确:并发池
use Illuminate\Http\Client\Pool;

$responses = Http::pool(fn (Pool $pool) => [
    $pool->as('users')->get('https://api.example.com/users'),
    $pool->as('posts')->get('https://api.example.com/posts'),
    $pool->as('comments')->get('https://api.example.com/comments'),
]);

$users = $responses['users']->json();
$posts = $responses['posts']->json();

Coolify 的真实用例:BunnyCDN 批量上传与缓存清除

SyncBunny 在把发布产物同步到 BunnyCDN 时,连续使用了两个 pool:

Http::pool(fn (Pool $pool) => [
    $pool->storage(fileName: "$compose_file_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
    $pool->storage(fileName: "$compose_file_prod_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
    $pool->storage(fileName: "$production_env_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
    $pool->storage(fileName: "$upgrade_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
    $pool->storage(fileName: "$upgrade_postgres_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_postgres_script"),
    $pool->storage(fileName: "$install_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
]);
Http::pool(fn (Pool $pool) => [
    $pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file"),
    $pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file_prod"),
    // ...其余 purge
]);

两个细节值得注意:其一,六个文件上传在同一个 pool 中并发完成,显著缩短了发布同步耗时;其二,上传与 purge(缓存清除)被拆成两个串行执行的 pool——上传必须全部成功后才清除 CDN 缓存,顺序不能颠倒,这正是「用 pool 并发、用顺序控制一致性」的工程化用法。该文件内还有单请求超时设置(第 55 行Http::timeout(30)第 351 行Http::timeout(10)),与 pool 组合使用。

五、测试中 Fake 所有 HTTP 调用

规范文档第五条:测试中绝不允许真实 HTTP 请求,应使用 Http::fake() 配合 preventStrayRequests()

it('syncs user from API', function () {
    Http::preventStrayRequests();   // 拦截任何未被 fake 的真实请求

    Http::fake([
        'api.example.com/users/1' => Http::response([
            'name' => 'John Doe',
            'email' => 'john@example.com',
        ]),
    ]);

    $service = new UserSyncService;
    $service->sync(1);

    Http::assertSent(function (Request $request) {
        return $request->url() === 'https://api.example.com/users/1';
    });
});

文档最后还提醒要覆盖失败场景:

Http::fake([
    'api.example.com/*' => Http::failedConnection(),
]);

Coolify 的测试实践

PullChangelogTest 完整演示了这套模式:先通过 config() 重写 releases_url 指向测试域名,再用 Http::fake() 按 URL 精确映射伪造响应,最后用 Http::assertSent() 断言确实发出了请求并落盘了 changelog 文件:

config(['constants.coolify.releases_url' => 'https://example.test/releases.json']);

Http::fake([
    'https://example.test/releases.json' => Http::response(fakeReleasesPayload(), 200),
]);

(new PullChangelog)->handle();

Http::assertSent(fn ($request) => $request->url() === 'https://example.test/releases.json');

该文件还通过伪造 payload 验证了业务分支(跳过 draft 为 true 的 release)。在整个 tests/Feature 目录下,Http::fake 被广泛使用(如 SyncBunnyTestVultrApiTestDigitalOceanApiTest 等 30+ 测试文件),是 Coolify 保证 CI 不依赖外部网络的统一手段。这也呼应了 SKILL.md 中第 8 条测试规则里「使用各类 fake」的总体原则。

六、小结:五条规则与 Coolify 源码对照表

规范要点 规范出处 Coolify 佐证
显式 timeout / connectTimeout 文档 §1 UpdateCoolifyServerTransferMigrator(120s 长请求)、OtherController(5s webhook)
服务级客户端宏 文档 §1 AppServiceProviderGitHub / GitLab
retry() + 退避、仅重试瞬时错误 文档 §2 CheckForUpdatesJobInitretry(3, 1000)
throw() / 状态码显式分支 文档 §3 UpdateCoolify 的「成功解析 → 失败降级到缓存 → 版本校验」
Http::pool() 并发 文档 §4 SyncBunny 批量上传 + 批量 purge
测试 Http::fake() + assertSent() 文档 §5 PullChangelogTest 等 30+ 测试文件

落地建议:在 Coolify 这类与多个外部服务(版本 CDN、CDN、云厂商、Git 平台)交互的 Laravel 应用中,HTTP 调用的可靠性不靠单一技巧,而是「超时定边界、重试扛抖动、状态分支管降级、pool 提吞吐、fake 保测试」五层叠加。编写新调用点时,先查同目录兄弟文件的既有模式(这正是 SKILL.md 中 Consistency First 原则的要求),再按本文的规则补全缺失的防御层。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341