kyzegs/guzzle-rate-limit-middleware 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

kyzegs/guzzle-rate-limit-middleware

Composer 安装命令:

composer require kyzegs/guzzle-rate-limit-middleware

包简介

A configurable Guzzle middleware for rate limiting HTTP requests based on response headers

README 文档

README

Guzzle Rate Limit Middleware banner

Guzzle Rate Limit Middleware

A configurable Guzzle middleware that prevents your application from hitting 429 Too Many Requests by reading rate-limit response headers and delaying requests before they exceed the limit.

State is persisted through a pluggable store, so rate limiting works across separate requests and processes — not just within a single operation.

Features

  • 🔧 Configurable headers — works with any API (Discord, GitHub, Twitter, the IETF RateLimit-* draft, or your own).
  • 💾 Cross-process state — share rate-limit state via PSR-16 (Redis, Memcached, Laravel/Symfony cache), the filesystem, or in-memory.
  • Pre-emptive delays — sleeps until a bucket resets instead of failing.
  • 🔁 429 retries — honours Retry-After and retries up to a configurable limit, then optionally throws.
  • 🪣 Bucket-hash discovery — adapts to APIs (like Discord) that assign buckets dynamically.
  • 🔒 Optional locking — plug in a distributed lock to serialise concurrent callers.
  • 🧪 Fully testable — the clock and sleeper are injectable, so timing is deterministic in tests.

Installation

composer require kyzegs/guzzle-rate-limit-middleware

Requires PHP 8.2+ and Guzzle 7.10+.

Quick start

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Kyzegs\GuzzleRateLimitMiddleware\RateLimitMiddleware;

$stack = HandlerStack::create();
$stack->push(new RateLimitMiddleware());

$client = new Client(['handler' => $stack]);

The default middleware reads the standard X-RateLimit-* headers and keeps state in memory.

Per-API presets

RateLimitMiddleware::github();   // X-RateLimit-* headers
RateLimitMiddleware::twitter();  // x-rate-limit-* headers
RateLimitMiddleware::ietf();     // RateLimit-* (IETF draft)
RateLimitMiddleware::discord();  // Discord headers + bucket-hash discovery

Cross-process rate limiting

To rate limit across separate requests/processes, give the middleware a persistent store. The recommended option is any PSR-16 cache:

use Kyzegs\GuzzleRateLimitMiddleware\RateLimitMiddleware;
use Kyzegs\GuzzleRateLimitMiddleware\Store\Psr16Store;

$middleware = RateLimitMiddleware::github(
    store: new Psr16Store($psr16Cache), // e.g. Redis, Laravel or Symfony cache
);

Or use the zero-dependency filesystem store:

use Kyzegs\GuzzleRateLimitMiddleware\Store\FilesystemStore;

$middleware = RateLimitMiddleware::github(
    store: new FilesystemStore('/var/cache/rate-limits'),
);

Available stores

Store Cross-process Notes
InMemoryStore (default) Lives for the PHP process only. Good for one long-running worker and tests.
FilesystemStore JSON files with atomic writes. No extra dependencies.
Psr16Store Wraps any Psr\SimpleCache\CacheInterface — Redis, Memcached, Laravel, Symfony, …

Custom headers

Header names live in the Headers config object:

use Kyzegs\GuzzleRateLimitMiddleware\Config\Headers;
use Kyzegs\GuzzleRateLimitMiddleware\RateLimitMiddleware;

$headers = new Headers(
    limit:      'X-API-Limit',
    remaining:  'X-API-Remaining',
    reset:      'X-API-Reset',        // absolute timestamp OR relative seconds
    resetAfter: null,                 // relative seconds (preferred when present)
    retryAfter: 'Retry-After',        // used for 429 retry delays
    bucket:     null,                 // enables bucket-hash discovery when set
    global:     null,                 // "true" indicates a global rate limit
    scope:      null,                 // "global" indicates a global rate limit
);

$middleware = RateLimitMiddleware::create(headers: $headers);

reset values below the year-2000 epoch are treated as relative seconds; larger values as absolute UNIX timestamps.

Behaviour options

use Kyzegs\GuzzleRateLimitMiddleware\Config\Options;
use Kyzegs\GuzzleRateLimitMiddleware\RateLimitMiddleware;

$options = new Options(
    maxRetries:          3,      // retries for a request that keeps getting 429
    safetyBufferSeconds: 1.0,    // added to every computed delay (clock skew/latency)
    jitterPercent:       0.0,    // random extra delay, 0-100% of the base delay
    throwOnRateLimit:    true,   // throw once retries are exhausted on a 429
    maxStoreTtl:         604800, // upper bound for cached bucket state (seconds)
    retryStatusCodes:    [429],  // statuses that trigger a retry
);

$middleware = RateLimitMiddleware::create(options: $options);

// Presets: Options::default(), Options::conservative(), Options::aggressive()

When retries are exhausted on a 429 and throwOnRateLimit is true, a Kyzegs\GuzzleRateLimitMiddleware\Exception\RateLimitExceededException is thrown (carrying the request, response, retry-after seconds and global flag).

Bucket resolution

Requests are grouped into buckets that share a rate limit. The default DefaultBucketResolver keys by METHOD host /path and collapses identifier-like path segments — numeric ids/snowflakes, UUIDs, and long hex tokens — to {id} (so /users/1 and /users/2, or two UUIDs, share a bucket). Human-readable slugs (e.g. /repos/{owner}/{repo}) are left literal because they're indistinguishable from route words; provide a custom resolver for APIs that bucket on such segments.

Provide your own by implementing BucketResolverInterface:

use Kyzegs\GuzzleRateLimitMiddleware\Contracts\BucketResolverInterface;
use Psr\Http\Message\RequestInterface;

final class MyResolver implements BucketResolverInterface
{
    public function resolve(RequestInterface $request): string
    {
        return $request->getMethod() . ' ' . $request->getUri()->getPath();
    }
}

$middleware = RateLimitMiddleware::create(resolver: new MyResolver());

Bucket-hash discovery (Discord)

Some APIs assign a request to a bucket dynamically and report it via a header (Discord's X-RateLimit-Bucket). When Headers::$bucket is set, the middleware stores state under the discovered bucket and re-keys automatically if the API reassigns a route. RateLimitMiddleware::discord() enables this together with a DiscordBucketResolver that respects Discord's major parameters (channel_id, guild_id, webhook_id and webhook_token).

Concurrency / locking

By default there is no locking. To serialise concurrent callers that share a bucket (e.g. multiple workers), implement LockFactoryInterface/LockInterface and pass the factory:

$middleware = RateLimitMiddleware::create(lockFactory: new MyLockFactory());

Testing your integration

The clock and sleeper are injectable, so you can assert delays without real waits. See tests/FakeClock and RecordingSleeper are good starting points.

Development

composer test      # PHPUnit
composer analyse   # PHPStan (level 6)

License

MIT License. See LICENSE.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-23

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固