Repository navigation
Improve Saloon correctness, caching and rate limiting - #55
Conversation
Ports Saloon's store, request property, query, config and timeout tests
(saloonphp/saloon v4 at 2f45e526f9d1) with their fixtures, merging the
existing Hypervel coverage into the upstream-named files under
tests/Saloon/{Unit,Feature,Fixtures}.
Requests keep the fluent API of Hypervel's HTTP client instead of
upstream's ArrayStore objects. The tests showed missing capabilities,
now added in that style:
- withoutHeader() and withoutHeaders() remove headers ignoring case.
- withoutQueryParameters() removes parameters added through the query
parameter methods. On a pending request it also clears the finalized
URI, so a removal after finalizeUri() takes effect.
- withoutOptions() removes options. Unlike a null option, removal lets
the connector value or the HTTP connection's configuration apply.
Removing a value from a request leaves connector defaults in place,
because connectors are read-only and merged into the pending request;
remove those from the pending request.
The HasTimeout plugin is ported. Plugins boot after connector and
request options are merged, so declared timeouts only fill options that
neither the declaring class nor the request sets. A request's declared
timeout therefore beats connector options, where upstream lets the
connector silently override it. Undeclared getters return null instead
of upstream's Config defaults, so the HTTP connection's timeouts apply.
IntegerRepository only held the request delay. HasDelay now stores a
nullable int, preserving the default hook and an explicit zero.
The README records the fluent API, read-only connectors and the
HasTimeout getter difference. The guide documents the removal methods
and HasTimeout. The six Saloon sync.yaml notes now describe how each
upstream repository maps into the unified package; checkpoints stay
unset until the reconciliation finishes.
Two upstream QueryParameterTest cases used a connector without default
query parameters, so they could not test overwriting one; they now use
QueryParameterConnector.
Validation: Saloon suite (393 tests) with ParaTest,
tests/Integration/Saloon against the engine test servers,
tests/Data/Saloon, TelescopeTagTest and AfterEachTestSubscriberTest;
composer analyse; php-cs-fixer.
Ports Saloon's body repository, body trait, multipart value and
serialization tests (saloonphp/saloon v4 at 2f45e526f9d1) with their
fixtures, merging the existing Hypervel coverage into the upstream-named
files under tests/Saloon/{Unit,Feature,Fixtures}. The two cases that call
upstream's live test API run against a new /mixed-multipart route on the
engine test server under tests/Integration/Saloon. This includes the
current HasMultipartBodyTest from saloonphp/saloon#561; that PR's sender,
retry and dependency changes remain for later slices.
Every request has the body methods, so there is no HasBody contract or
ChecksForHasBody check. ArrayBodyRepository stays abstract, because
upstream's concrete class throws when converted to a stream, and
MultipartBodyRepository::boundary() is getBoundary() again, as upstream.
Defects fixed:
- attach() read the body property directly, so attaching to a request
whose default multipart body was not yet loaded discarded it.
asMultipart() also discarded attached values; both now keep an
existing multipart body.
- A multipart body kept any existing Content-Type, so a JSON body
trait's default, an earlier asJson() or a connector's JSON default
header sent the body without its boundary. A content type without a
boundary is now replaced, in prepared requests and pre-send PSR
snapshots; one that declares a boundary is kept. The HTTP client had
the same defect for Http::asJson()->attach() and connection default
headers, and now removes such a header from multipart requests so
Guzzle adds the boundary.
- Guzzle streams close their resource when destroyed. A PSR snapshot
taken in request middleware or a SendingSaloonRequest listener
therefore closed a resource body or attached file before sending.
StreamBodyRepository now wraps a resource once and shares the wrapper
across copies; MultipartValue caches its wrapper at first
serialization.
- List-valued multipart part headers, which SDK-generated forms pass,
failed in Guzzle; they are joined like repeated HTTP fields.
- Two upstream tests checked all() instead of get(), or never called the
set() they described.
attach() also accepts an array of argument tuples, like Http::attach().
withData() on a multipart body appends the values as fields, like
Http::attach()->post($url, $data). For that, MultipartValue also accepts
booleans, null and arrays without a filename or headers, which Guzzle
sends as fields; upstream's invalid empty-array case is now valid.
The README records the body differences, and the guide documents the
multipart behavior, the array form of attach() and connector-owned
JSON flags.
Validation: Saloon suite (470 tests) with ParaTest, tests/Http
(1,018 tests), tests/Integration/Saloon and tests/Integration/Engine
against the engine test servers, tests/Data/Saloon; composer analyse;
php-cs-fixer.
Ports Saloon's request, connector, pending request, URL helper, clone, conditionable, plugin and API version tests (saloonphp/saloon v4 at 2f45e526f9d1) with their fixtures, merging the existing Hypervel coverage into the upstream-named files under tests/Saloon. Cases that call upstream's live test API run against new /user and /error routes on the engine test server under tests/Integration/Saloon. This completes saloonphp/saloon#526, #549 and #564 and the QUERY method commit 538994a591, which Hypervel already supported, plus the request and URL parts of #539 and #542. New APIs: - PendingRequest::withMethod() and withUrl() change one operation without touching the request. uri() replaces upstream's getUrl(). - withUrlParameters() fills URI template placeholders in the base URL, endpoint or URL override, as Hypervel's HTTP client does. The package now requires guzzlehttp/uri-template directly. - HasApiVersion and VersionMode. Connectors are read-only, so there is no setApiVersion(); a class assigns its version in the constructor or overrides getApiVersion(). A request's version replaces its connector's in every mode. - createPendingRequest() on the manager, connectors and requests prepares a request without sending it. Mock clients are still chosen when the request is sent, so it takes no mock client. - HasConnector, which SoloRequest now uses. Defects fixed: - Authentication ran before plugins, so a cookie authenticator recorded an API-version host template before the version plugin filled it in, and the cookie was never sent. Plugins now boot first, as upstream. An authenticator a plugin selects is applied once after every plugin boots, and the authenticator it replaced is no longer applied. - Relative endpoints with a colon in the first segment, such as documents:batchGet, were parsed as a scheme and rejected, and //search as a host. Only scheme:// endpoints are absolute now. - Merging query parameters into a URL with empty query segments produced && or a leading &. - A header written as a list item, such as ['Accept: application/json'], caused a TypeError in a JSON-body request. It now fails with upstream's message when the pending request is created. - Upstream's Conditionable argument tests asserted inside the callbacks, so they passed if the callbacks never ran. Upstream's Macroable tests are covered by the framework's SupportMacroableTest, which gains private member cases. The README records the kept differences: resolveResponseClass(Response), allowsBaseUrlOverride() methods, no mock client on createPendingRequest(), and pending withMethod() and withUrl(). The guide documents the new APIs, endpoint path rules and the order of plugins and authentication. Validation: Saloon suite (574 tests) with ParaTest, tests/Integration/Saloon and tests/Integration/Engine against the engine test servers, tests/Data/Saloon, SupportMacroableTest and FacadeDocblocksTest; composer analyse; php-cs-fixer.
Ports Saloon's config, sender, Guzzle sender, response, request exception, PSR, AlwaysThrowOnErrors, CastsToDto, data object wrapper and debug tests (saloonphp/saloon v4 at 2f45e526f9d1) with their fixtures, merging the existing Hypervel response, sender and debug tests into the upstream-named files. Cases that call upstream's live test API run against the engine test server under tests/Integration/Saloon. This completes saloonphp/saloon#522, #525, #534, #535, #537, #552 and #554, plus the sender test changes of #561 and the config parts of #518 and #557. #475 (a default handler stack) is excluded, since HTTP connections own the transport handler. New APIs: - Upstream's 14 status exceptions, such as NotFoundException and TooManyRequestsException, chosen by status in Response::newRequestException() after getRequestException(). They extend ClientException or ServerException, so existing catches still apply. - Response::isJson(), isXml() and array(). Defects fixed: - A transfer that failed after the response headers arrived escaped Saloon's error handling for a 4xx or 5xx status, as the HTTP client's RequestException. Sender now reports every incomplete transfer as a connection failure, so it throws FatalRequestException, runs the fatal middleware and is retried. - dtoOrFail() uses upstream's message. Adaptations, recorded in the README: - Upstream's Config maps to Saloon::middleware(), the saloon.connection settings, the framework Date, Sleep::fake() and Http::preventStrayRequests(). Sender, clock and sleep resolver cases are removed. - Responses keep the HTTP client's header(), headers(), json() and object(), and their getters drop upstream's get prefix. - Request exceptions extend the HTTP client's, so getRequestException() receives no sender exception and custom messages override prepareMessage(). - Connector debuggers register from boot(). die: true throws Swoole's ExitException inside a coroutine, so there is no die handler. - xmlReader() is excluded: it only wraps XML Wrangler, whose dependency tree and Laravel collection helpers do not fit Hypervel. The docs show XmlReader::fromPsrResponse($response->toPsrResponse()). Also adds the missing setup and teardown docblocks in the Saloon tests. Validation: Saloon suite (665 tests) with ParaTest, tests/Integration/Saloon and tests/Integration/Engine against the engine test servers, tests/Data/Saloon; composer analyse; php-cs-fixer.
Ports Saloon's middleware pipeline, pipeline, retry connector, retry request and delay request tests (saloonphp/saloon v4 at 2f45e526f9d1), merging the existing Hypervel pipeline and delay tests into the upstream-named files. Live retry cases run against a new /header-error route on the engine test server. This completes saloonphp/saloon#557 and the retry test changes of #561. New APIs: - defaultRetryPolicy() on connectors and requests, like defaultDelay(). An explicit request policy wins, then the request default, then the connector default. A request's policy replaces the connector's entirely, so retry(1) turns off connector retries. Defects fixed: - Middleware pipelines formed a reference cycle through their wrapper closures, so they stayed alive until garbage collection. The wrappers are now static. - A mock response that threw FatalRequestException skipped the fatal middleware and retries. It now runs the fatal pipeline once and follows the retry policy, keeping the thrown exception. - The request's middleware ran first and global middleware last. Each send now builds its own pipeline in upstream's order: global, plugins, connector boot(), request middleware, request boot(). The global and request pipelines are never changed, so repeated sends and retries do not accumulate middleware. - Connectors could only set retries from boot(), which overrode a request's explicit retry(). Connector defaults now come from defaultRetryPolicy(). - Mocked responses skipped the request delay. The delay now runs before every attempt, real or fake, but not for cache hits. Adaptations, recorded in the README: - Middleware contracts are typed, a returned pending request is ignored, and the pipeline getters drop the get prefix. - Upstream's retry properties and handleRetry() map to retry() and defaultRetryPolicy(). Request changes move to the when callback through $pendingRequest->request(), and exponential backoff to a delay closure. - Deprecated sendAndRetry() is not ported; its exponential backoff case moves to RetryRequestTest. - Fewer than one attempt or a negative interval throws. Also adds the missing method titles and closure return types in SaloonManagerTest. Validation: Saloon suite (728 tests) with ParaTest, tests/Integration/Saloon and tests/Integration/Engine against the engine test servers, tests/Data/Saloon; composer analyse; php-cs-fixer.
Adds title docblocks to helper methods in Saloon test files changed by the earlier reconciliation commits, including the fixture name data provider's return type. Docblock-only; each file's tests pass.
Ports Saloon's authenticator, authenticates-requests, access-token authenticator, OAuth config, auth-code flow, client-credentials flow and absolute OAuth endpoint tests (saloonphp/saloon v4 at 2f45e526f9d1) with their fixtures, merging the existing Hypervel authentication, OAuth config and OAuth grant tests into the upstream-named files. The OAuth request body tests move to Unit/Oauth2. This completes saloonphp/saloon#516, #518's OAuth expiry cases, #539's access-token authenticator and #542's OAuth endpoint cases. Defects fixed: - Token expiries were typed as CarbonInterface, so code passing a plain DateTimeImmutable, as upstream does, failed, and a mutable Date configuration produced mutable expiries. The contract, AccessTokenAuthenticator and the grant factories now take ?DateTimeImmutable, and framework expiries copy the Date clock into an exact CarbonImmutable. - The authenticators were readonly classes, so subclasses that add state failed to compile. They keep read-only properties instead. AccessTokenAuthenticator now builds its header from getAccessToken(), so an overridden getter reaches the request. - The client-credentials grant shared the auth-code factory signature, so upstream's documented two-argument override lost the expiry or failed. Each grant now owns upstream's factory signatures, and ParsesOAuthTokenResponses only parses and validates the response. - A PKCE verifier was only added by the default token request, so a custom resolveAccessTokenRequest() dropped it. The verifier is now added to the resolved request before the modifiers run, and resolveAccessTokenRequest() takes upstream's two arguments. - Basic authentication was a transport option, so mocks, middleware and assertions could not see it, and combining it with a token sent a different header from upstream. It now sets the Authorization header. - Authorization URLs used a different parameter order, dropped falsy extra parameters such as max_age=0, and could not replace standard parameters such as response_type. They now follow upstream's order, keep falsy values and let extra parameters replace everything except state. - Null and empty scopes are dropped from authorization URLs and client-credentials token requests alike, through OAuthConfig::scopes(), and a "0" scope is kept. Client-credentials requests previously sent stray separators. Adaptations, recorded in the README: - OAuthConfig::validate() returns true and the validation and refresh messages match upstream; getUser() takes the OAuth authenticator. - Deprecated withTokenAuth(), withQueryAuth(), withHeaderAuth() and withCertificateAuth() are not ported, withDigestAuth() takes no digest type, and authenticator() replaces getAuthenticator(). - Authenticator properties stay read-only because an authenticator may be shared by concurrent requests. Validation: Saloon suite (815 tests) with ParaTest, tests/Integration/Saloon against the engine test servers, tests/Data/Saloon; composer analyse; php-cs-fixer; the private SDK generator suite against this branch.
Ports Saloon's mock client, mock client assertion, mock response, mocks-requests, global mock client, mock config, fixture data, fixture path traversal, simulated response payload and mock request tests (saloonphp/saloon v4 at 2f45e526f9d1) with their fixtures, merging the existing Hypervel mock client, fake response, fixture and recorded response tests into the upstream-named files. This completes saloonphp/saloon#539's fixture, storage and mock changes, #548 (MockClient::withoutCache()) and #563 (typed assertSent closures). Defects fixed: - URL patterns for mock responses, URL assertions and allowed stray requests needed the full URL, so upstream's path patterns such as '/user' or 'api.example.com/*' never matched. One matcher now prefixes the pattern with '*' and matches the URL with or without its query string; a pattern that includes a query still matches only that query. - Typed assertion closures only understood class names and unions, so a mixed, object or intersection parameter threw. They now use upstream's recursive type check: mixed and object match any request, other built-in types none, and intersections need every part. - Fixture header redaction rules were applied to whole header values, so upstream-style closures failed on list headers and headers with several values, such as Set-Cookie, were stored unredacted. Rules now apply to each value, case-insensitively, and only closures are called, so callable-looking strings stay literal. - Fixtures whose data is a JSON object failed to load. Array data now survives loading, replay and merging; storing encodes it before redaction, and base64 applies only to binary strings. - A fixture's own context overrode the recorded response's context, including beforeSave() changes. The recorded context now wins without changing the fixture. - Nine request fixtures declared upstream's $connector property and DefaultPropertiesRequest a defaultData() method, though nothing reads either. Both are removed. Upstream changes: - MockClient::withoutCache() keeps requests that reach the transport, such as recording a missing fixture, out of the response cache. - NoMockResponseFoundException uses upstream's message. Adaptations, recorded in the README: - Connectors take no mock client: attach one to the request, pass it to send() or use Saloon::fake(), which also backs MockClient::global(). - recorded(), lastRequest(), lastPendingRequest(), lastResponse() and match() replace upstream's getters; assertSentJson() is not ported. - MockConfig maps to the saloon.fixtures.path configuration, fixtures are read and written through Filesystem instead of upstream's Storage helper, getFixturePath() is absolute, and recorded fixtures list every header value. Validation: Saloon suite (895 tests) with ParaTest, tests/Integration/Saloon against the engine test servers, tests/Data/Saloon; composer analyse; php-cs-fixer. The private SDK generator was checked by source: it uses only APIs this change keeps.
Ports Saloon's pool tests (saloonphp/saloon v4 at 2f45e526f9d1) and maps its asynchronous request tests onto pools, merging the existing Hypervel pool tests into the upstream-named files. Upstream's live pool case runs against the engine test server and replaces SaloonClientTest's pool case. Defects fixed: - Pooled requests ignored throwable responses: a 4xx or 5xx response, or one a request or connector failure hook marks as failed, went to the response handler and came back from send() as a success. Pool requests now call throw(), so every response toException() treats as failed reaches the exception handler, or PoolException::failures() when there is none. Upstream's real asynchronous path checks these hooks only after Guzzle rejects a transfer; Hypervel applies the rule to every response. - PoolException had no previous exception unless scheduling failed, so reported pool failures hid their cause. It now chains the failure that stopped scheduling, otherwise the first request failure, or the first callback failure if no request failed. Adaptations, recorded in the README with source comments where upstream methods are omitted: - Pools send through coroutines instead of promises: there is no sendAsync(), isAsynchronous(), setAsynchronous() or promise item. send() returns the successful responses keyed and ordered by input, and process() sends without keeping them. - Concurrency is a positive integer; upstream's callable and unlimited forms are not supported. - Handlers receive the response or exception and the key, without the aggregate promise. - Unhandled request, callback and item failures throw PoolException once every started request has finished. - Ending a request generator stops further requests, but one already waiting for a slot still starts. - requests() replaces getRequests() and runs a request callback on each send. The pool docs now cover failed responses, the exception cause, sending a pool again and stopping early. Validation: Saloon suite (910 tests) with ParaTest, tests/Integration/ Saloon against the engine test servers; composer analyse; php-cs-fixer. The private SDK generator was checked by source: it uses pools only through paginators, whose throw middleware already fails pages.
Ports the Laravel plugin's events, mock request, simulated response
payload and mock client assertion tests (saloonphp/laravel-plugin at
47d06ef8621a) into tests/Saloon/Hypervel on the core fixtures, which
the plugin's fixtures copy. The plugin's pool and request tests repeat
core cases and are covered by the core tests, noted there.
Defects fixed:
- Event::fake() and Event::fakeFor() did not reach Saloon or the HTTP
client once they were resolved, because SaloonManager and the HTTP
client factory keep the dispatcher they were built with. Both
providers now rebind the events service onto the resolved services
(setEventDispatcher(), setDispatcher()), as Laravel's
AuthServiceProvider does, so fakes apply and are removed afterwards.
Both Saloon request events use Dispatchable.
- Saloon::fake() replaced the global mock client, discarding earlier
fakes, recorded responses and settings. It now adds responses to the
existing client, as upstream; passing a MockClient replaces it.
- A null integrations namespace always produced App\Http\Integrations,
whatever integrations path was configured. It now follows the path
beneath the application directory, and a path outside it requires
the namespace unless --target-namespace is given.
- saloon:list garbled endpoints that used interpolation, concatenation,
dots inside literals or escaped quotes, such as SharePoint's
getbytitle('...'), and did not read double-quoted base URLs.
Not ported, recorded in the README with source and test notes: the
plugin's deprecated response recorder, its MockClient subclass and the
deprecated assertSentJson() facade method.
Validation: Saloon suite (ParaTest), tests/Http, the HTTP client
consumers in Integration/Http, Reverb webhooks and Inertia, Data's
Saloon tests, FacadeDocblocksTest, composer analyse and php-cs-fixer.
The private SDK generator was checked by source: it uses neither
global fakes nor the generators.
The Laravel plugin (saloonphp/laravel-plugin at 47d06ef8621a) adds Telescope, Pulse and Nightwatch recording middleware to its own Guzzle sender. Saloon here sends through the HTTP client, so Telescope's ClientRequestWatcher already records its requests with the saloon, connection and request tags, and the plugin's Telescope middleware would duplicate it. Hypervel has no Pulse or Nightwatch package. The README, service provider and sender test record these exclusions. Coverage added for the plugin's cases the existing tests did not assert: - ClientRequestWatcherTest decodes hal+json (with and without a charset) and vnd.api+json request bodies, and decodes and masks a hal+json response. - GuzzleSenderTest checks that Http::globalMiddleware() runs on Saloon requests, which OpenTelemetry's HTTP client instrumentation relies on. - TelescopeTagTest asserts the Turbopuffer tag value. Validation: ClientRequestWatcherTest, GuzzleSenderTest, SenderTest, TelescopeTagTest, the Saloon suite (ParaTest) and php-cs-fixer.
The cache plugin (saloonphp/cache-plugin at 5a036bae401d) reads the cache before mock responses are matched, lets MockClient::withoutCache() skip the cache (#18), and adds clearCache() to forget a cached response without sending the request (#17). - SaloonManager::send() now reads or invalidates the cache before matching the mock client, so a hit consumes no mock response or fixture. Mock, fixture and transport responses share one write path, and withoutCache() bypasses reads, writes and invalidation. A fake supplied by request middleware still bypasses the cache. - Responses are written unless failed(), as upstream, so request and connector failure hooks apply. Previously a hook-rejected 200 was cached and replayed to every retry. - SaloonManager::clearCache() prepares the request as send() does and forgets the key from the same store, key and scope. Connector and HasCaching delegate to it; native parameter types replace upstream's wrong-counterpart exceptions. - The service provider allows CachedResponse through the cache's serializable-class policy. Without it, every serializing store (file, database, Redis) missed silently. - Cache hits are no longer recorded on the mock client, matching upstream, since nothing was sent. Cache stores replace the plugin's drivers, so its PSR adapter test is REMOVED and its assertions are covered by the native-store tests, and the Laravel driver test is a file-store round trip. Upstream's real-request case runs against the engine test server. The laravel plugin (#76) fixed its Telescope middleware's return type for scalar JSON response bodies; its body sanitizing still records them as HTML or empty, as Telescope does. ClientRequestWatcher and RequestWatcher now record valid scalar JSON under a JSON media type as sent, within size limits, and label only an empty body as empty. Validation: the Saloon and Telescope suites (ParaTest), tests/Integration/Saloon on the engine test servers, the changed test files after review corrections, FacadeDocblocksTest, composer analyse and php-cs-fixer.
Ports the pagination plugin's feature tests (saloonphp/pagination-plugin at a61822ca93c1) into tests/Saloon/Pagination/Feature, running against an Http::fake of the public superhero endpoints they call. #17's rewind case is ported in CollectTest; rewind() already cleared the body checksums. Defect fixed: the infinite-loop check was a response middleware closure bound to the paginator, so the paginator's own request held the paginator through its pipeline. A dropped paginator stayed in memory until the cycle collector ran, and any kept response kept it alive too. The check now runs in Paginator::current() after the send and before item mapping, keyed by page so a mapping retry replaces its checksum, and is skipped while pooling. Upstream's protected getBodyChecksum() is restored (xxh128 of the body), so APIs that add a per-request value can compare page contents only, and so is upstream's exception message, naming pages instead of requests. The guide shows the override. Test adaptations: async() and promise iteration are REMOVED, with the async cases traversing through pool(); the imported offset connectors end at getOffset() >= total and the async total pages round up, so a partial last page ends correctly. PaginatorTest drops cases now covered upstream and adds the dropped-paginator, checksum override and request-middleware cases. The README records the renamed and omitted paginator methods, pool() in place of async pagination, unpoolable cursors and zero-based keys counted from the start page. Validation: each changed test file, the Saloon suite (ParaTest), composer analyse and php-cs-fixer. A scratch run at 7 per page confirmed the corrected fixtures fetch 3 pages with all 20 items.
Ports the rate-limit plugin's feature, unit and job middleware tests
(saloonphp/rate-limit-plugin at aed5ff488e4d) into
tests/Saloon/RateLimit/{Feature,Unit,Hypervel/Feature}, sending through
Http::fake() because Saloon fakes and cache hits make no provider call
and consume no capacity.
- A 429 whose Retry-After is missing, malformed or too large for the
rate limiter records upstream's 60-second cooldown (#28). A valid
zero or past delay records none.
- waitForRateLimits() now receives the denied policy and its decision,
so a resource can wait out a burst limit and still throw for a daily
quota. Nothing is resent from middleware: waiting for Cooldown with a
429 retry policy waits out the cooldown and sends again through the
manager's bounded attempt loop with the original connector (#27, #29).
- resolveRateLimiterName() (default: the class name) names the limiter
saloon:{name} for admission, cooldowns and inspection, so resources
returning the same name share a quota and its cooldown. The default
cooldown key is empty, so cooldowns follow the name.
- Saloon::inspectRateLimit() checks a resource's policy or cooldown
without consuming it, through the send path's store and name, in
place of hasReachedRateLimit() and getExceededLimit().
- rateLimitingEnabled(PendingRequest) skips a resource's admission,
cooldown checks and cooldown recording for one operation, in place of
useRateLimitPlugin() and $rateLimitingEnabled.
- RateLimitReachedException names the limiter, with the policy key when
set, so a log shows which integration was blocked.
Test adaptations: the Limit::custom() case is REMOVED (custom responses
override resolveRateLimitCooldown()), and so is the unconfigured-limit
case (rejected when the limit is created). TestConnector takes the
limits to wait for in place of sleep(). JobMiddlewareTest dispatches
real jobs through ReleaseOnRateLimit and absorbs ReleaseOnRateLimitTest.
RateLimitTest drops the cases now covered upstream. The README and the
guide cover sharing, inspecting and disabling limits and the 60-second
fallback; the sync note records the test mapping.
Validation: each changed test file, the Saloon and RateLimiter suites
(ParaTest), FacadeDocblocksTest, composer analyse and php-cs-fixer.
The rate-limit plugin (saloonphp/rate-limit-plugin at aed5ff488e4d) offers limits that reset on the clock: untilEndOfMinute(), untilEndOfHour(), untilMidnightTonight(), everyDayUntil() and untilEndOfMonth(). Hypervel's rate limiter only had windows measured from the first operation, so a provider quota that resets at midnight could not be matched. CalendarWindow is a new admission policy that resets at the start of each minute, hour, day (at an optional HH:MM[:SS] time) or month, in the application's timezone or one given with timezone(). It follows the local clock across daylight saving changes: - Hourly windows are clipped at offset changes, so a repeated hour is its own window and half-hour shifts (Lord Howe Island) do not overlap. - A daily reset time that happens twice resets at its first occurrence, resolved explicitly because PHP's parser picks different occurrences in different zones. A skipped reset time moves forward that day. Stores count a calendar window like a fixed window; new state expires at the window's end. The Redis store passes the window's start and end computed from Redis TIME, and the script rejects a stale window before writing, so a PHP clock in another period cannot charge the wrong one. A stale group discards its tentative charges and retries. Key identity includes the period, reset time and timezone. Upstream's Limit, Bucket, LimitStore and store tests map case by case to the rate limiter's store contract and integration tests; Unit/HelperTest is ported and absorbs RateLimitTest's Retry-After parsing cases. There are no file, cache or Predis stores: a general cache can't apply the policies atomically, and Redis uses PhpRedis. Identical policies share state instead of throwing upstream's duplicate-name exception. The README, rate-limiting guide and sync note record the mapping. Validation: CalendarWindowTest, the store contract on worker-array, Swoole, database (SQLite, MySQL, MariaDB, PostgreSQL) and Redis stores, KeyResolverTest, LimiterTest, the Saloon rate-limit tests, the RateLimiter and Saloon suites (ParaTest), composer analyse and php-cs-fixer.
Requests were SelfBuilding with a newInstance() that called new static(), so app()->make() on a request ignored make() parameters and constructor injection and threw ArgumentCountError for any request with constructor arguments. Request now implements Transient, which the container added after the request lifecycle: unbound resolutions stay fresh while dependencies are injected and parameters apply. A comparison of saloonphp/saloon's public surface (v4 at 2f45e526f9d1) with the port found: - PendingRequest took a cache factory and rate limiter it never used, through cache() and rateLimiter() getters with no callers. Both are removed from its constructor. The lifecycle methods only the manager calls are marked @internal, like createPsrRequest(). - PipeOrder had renamed upstream's FIRST and LAST cases without approval; upstream's names are restored. - withUserAgent() and withoutVerifying() had no tests. - Several renamed or omitted upstream members were missing from the README: the get-prefixed request and pending-request getters, setBody(), hasFakeResponse(), getResponseClass(), sender and PSR factory arguments, fromPsrResponse(), createException(), withBufferedBody(), getSenderException(), getStatusMessage(), send()'s $handleRetry argument, and request exceptions no longer being SaloonExceptions. Source comments mark the omitted members. The Laravel porting guide gains a Saloon section covering the namespaces, read-only connectors and fluent methods, the transport and the exception catch. Validation: RequestTest, HeadersTest, ConfigTest and each changed test file, the Saloon suite (ParaTest), tests/Data/Saloon, the Saloon integration tests on the engine servers, composer analyse and php-cs-fixer.
SaloonManager exposed sender(), cache() and rateLimiter(), which returned the injected HTTP sender, cache factory and rate limiter unchanged. Nothing called them, and none carried Saloon-specific configuration: the cache and rate-limiter services are available through the framework's own facades and contracts, and Saloon's own operations on them stay available through clearCache() and inspectRateLimit(). The getters and their facade annotations are removed. The package no longer requires hypervel/reflection, which it never used. The package metadata test now lists guzzlehttp/uri-template, which the package already requires. Saloon's Guzzle 8 and PSR-7 3 constraints are unchanged; the suite runs on Guzzle 8.2. The make:saloon stubs regain upstream's placeholder comments and the authenticator's empty constructor. The HTTP connection config comment now describes the connection plainly. Three unit tests still built pending requests through a one-line helper left from the previous constructor change; they now construct PendingRequest directly. A send benchmark without network access measured about 200 us per Saloon request against about 130 us for the same HTTP client call, spread over small steps; no change is needed. Validation: each changed test file, the Saloon suite (ParaTest), tests/Data/Saloon, FacadeDocblocksTest, Composer/PackageManifestConsistencyTest, composer analyse and php-cs-fixer.
Comparing the public and protected surfaces of upstream's cache, pagination and rate-limit plugins with Hypervel's found no code gaps, but the README did not name the Hypervel equivalents for several upstream APIs. It now covers CacheKeyHelper, the cache middleware and expiry methods, LimitException, Limit::allow()'s threshold, Limit::custom(), RetryAfterHelper, $detectTooManyAttempts, the exceeded-limit hooks and per-user limit names. CacheKey is no longer final, since it protects no invariant. A short comment explains why it hashes with sha256 instead of xxh128: the request identity includes credentials, so a forged collision would return another caller's cached response. The private SDK generator and generated SDKs were verified against this branch, with two test updates made in that repository. Validation: the Saloon suite (ParaTest), tests/Saloon/Cache, composer analyse and php-cs-fixer; the private SDK generator suite, its analysis configurations, the seven generated SDK test suites and generated-code analysis.
The guide was compared with Saloon v3's documentation from connectors through concurrency and testing, and every claim and example was checked against Hypervel's source. Corrections: the shouldThrowRequestException example could never stop a 404 from throwing, since a response throws when either the request's or the connector's hook returns true. The guide now separates failure classification from throwing, retries and caching. Request data conversion applies to Hypervel's Stringable, not any stringable value. The differences list no longer mentions OAuth 1, which upstream never had. Added coverage includes the HTTP connection defaults and rejected request-shaping options, standalone request credentials, query string encoding, built-in and combined authenticators, multipart bodies, request-instance middleware and fake responses, data objects, the status exception table, retry forms and exhaustion, debug output, OAuth endpoint defaults, scopes, state and token storage, pool concurrency, mock client assertions and fixture recording. Fixture recording stays subject to Http::preventStrayRequests(); the guide and README say to allow the API's URLs while recording. Validation: documentation only; every in-page link resolves to an anchor.
The rest of Saloon v3's documentation was compared with the guide: pagination, caching, rate limits, the Laravel plugin, building plugins, the how-to guides and the upgrade and known-issue pages. Every claim and example was checked against Hypervel's source. Added: plugin boot methods must be public, plugins change the pending request, and a plugin on both a connector and its request boots twice. Fetching a token from a connector's boot method, which can't be combined with RequiresAuth because its check runs first. Choosing the cacheable methods. Paginators yield one page response at a time, keep only that page in memory and throw for a failed page; iterator_count cannot find the last page; the default page size, offsets and custom paginators. Saloon accepts fixed, calendar and sliding windows and leaky buckets, per-user by keys, the exception's wait time, the default store and disabling 429 cooldowns. Requests require TLS 1.2 unless the connection changes crypto_method. The README's differences heading now names Saloon and lists all five upstream repositories. The sync notes say where future upstream changes, plugin test cases and documentation belong, and which upstream pages have no Hypervel equivalent. A colon in one note made sync.yaml invalid YAML; it parses again. Validation: documentation only. A nested send from a connector's boot method was confirmed with a temporary test. Every guide link and README anchor resolves.
The Saloon tests that 0.4 updated for PHPUnit's exception message assertions were consolidated into upstream-named tests on this branch, so they stay deleted. The Telescope watcher tests keep this branch's added cases with 0.4's void return types.
PHPUnit 13.3 deprecates expectExceptionMessage(). The Saloon and calendar-window tests changed on this branch now use expectExceptionMessageIs() for complete messages, which is stricter, and expectExceptionMessageIsOrContains() where a test checks part of a message: delay validation, the base-URL replacement message's first sentence, and a missing fixture's environment-dependent path. The fixture-name and duplicate-pipe expectations now include the messages' final period.
Upstream Saloon's Laravel plugin, laravel-data and laravel-permission each ship a root ide.json with code-generation templates for the Laravel Idea PhpStorm plugin. Hypervel does not ship this metadata for any of them. The Saloon README lists it with the other plugin features that are not included and points to the saloon:* generator commands instead. The Data and Permission sync notes record it, since neither README covers it.
Set the checkpoints for Saloon and its four plugins (Laravel, cache, pagination and rate limiting), plus the shared Saloon docs: - saloonphp/saloon v4 through 2f45e526f9d1 (#564) - saloonphp/laravel-plugin v4 through 47d06ef8621a (#89) - saloonphp/cache-plugin v3 through 5a036bae401d (#17) - saloonphp/pagination-plugin v2 through a61822ca93c1 (#17) - saloonphp/rate-limit-plugin v2 through aed5ff488e4d (#31) - Sammyjo20/saloon-docs v3 through 8ea96847141f (#92)
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTip of the day💡 Did you know, you can route each severity your way: inline, summary, both, or drop |
PR Summary by QodoReconcile Saloon with upstream v4: correctness, caching and rate-limit fixes
AI Description
Diagram
High-Level Assessment
Files changed (33)
|
|
The Redis store test for calendar windows read the server clock with time() without a node. PhpRedis's RedisCluster::time() needs a key or address, so the test failed on the Redis Cluster job. It now reads the clock of the node that holds the window's key. The store itself reads TIME inside its Lua script and was not affected. Retrying a request rewinds its body to where the first attempt started, but nothing tested it. New retry tests send a resource body that starts part way through, attach a resource file, and use a non-seekable body. The first two fail if the rewind is removed. The third checks that a non-seekable body throws BodyException with the failed response's exception as its previous one, instead of resending an empty body. Verified on a three-node Redis Cluster and a standalone Redis 8 server.
Without a configured integrations namespace, the generator commands map
the integrations path beneath the app directory to a namespace. They
compared the raw configured path, so app_path('Http/../Integrations')
produced a namespace containing "..", and app_path('../src/Integrations')
passed the inside-the-app check and generated an invalid namespace
instead of asking for one.
The commands now canonicalize the configured path with Symfony's
Path::canonicalize() and use it for the namespace, the destination file
and the integration suggestions. The app path is canonicalized the same
way before the comparison. Saloon now requires symfony/filesystem
directly; the root package already did.
The generator tests cover an in-app path with dot segments and one that
leaves the app directory.
The SDK connector, resource and HasConnector examples used GetUser, GetRepository and GitHubConnector without importing them. Copied as shown, the classes resolve in the wrong namespace: the connector method fails with a missing class, and GitHubConnector::class silently names a class that does not exist until the request sends.
This improves Saloon's request handling, authentication, caching, pagination and concurrent requests, and adds rate limits that reset on the clock.
Failed responses now stay out of the cache and reach pool failure handlers instead of counting as successes. Multipart requests keep their body, attached files and boundary while the request is prepared. OAuth requests and middleware order match Saloon, and mock URL patterns such as
/usernow match. A rate limit can reset at the start of each minute, hour, day or month, like a provider quota that resets at midnight. The Saloon guide now covers the whole package, including its cache, pagination and rate limit plugins.A few shared framework classes change too: the HTTP client drops a multipart
Content-Typethat has no boundary and accepts a replaced event dispatcher, Telescope shows scalar JSON bodies as sent, and the rate limiter gains aCalendarWindowpolicy.Requests, URLs and bodies
documents:batchGet, were read as a URL scheme and rejected, and//searchwas read as a host. Onlyscheme://endpoints are absolute now. Merging query parameters into a URL with empty segments no longer produces&&or a leading&.withoutHeader(),withoutHeaders(),withoutQueryParameters()andwithoutOptions(). Removing an option lets the connector or HTTP connection value apply, unlike setting it tonull.withUrlParameters()fills URI template placeholders in the base URL, endpoint or URL override, as the HTTP client does.withMethod()andwithUrl(), which change one operation without touching the request. Connectors, and requests that useHasConnector, gaincreatePendingRequest()to prepare a request without sending it.HasApiVersionandVersionModeadd Saloon's API versioning,HasTimeoutits declared timeouts, andHasConnectorits request-owned connector, whichSoloRequestnow uses.asMultipart()discarded attached files. Both keep the existing body now.attach()also accepts an array of argument tuples, likeHttp::attach().Content-Type, so a JSON default header sent the body without its boundary. A content type without a boundary is now replaced.SendingSaloonRequestlistener closed a resource body or attached file before it was sent. Stream bodies and multipart values now keep one wrapper.['Accept: application/json'], caused aTypeErrorin JSON requests. It now fails with Saloon's message when the pending request is created.Sending, retries and errors
NotFoundExceptionandTooManyRequestsException, are added. They extendClientExceptionorServerException, so existing catches still apply. Responses gainisJson(),isXml()andarray().FatalRequestException, runs the fatal middleware and is retried.defaultRetryPolicy(), likedefaultDelay(). Connectors could only set retries inboot(), which overrode a request's explicitretry(). A request's policy now replaces the connector's, soretry(1)turns connector retries off.boot(), request middleware, then the request'sboot(). Repeated sends and retries no longer add middleware twice.FatalRequestExceptionskipped the fatal middleware and retries, and mocked responses skipped the request delay. Both now behave like real sends. Cache hits still skip the delay.Authentication and OAuth
Authorizationheader.CarbonInterface, so passing a plainDateTimeImmutable, as Saloon's examples do, failed. The contract,AccessTokenAuthenticatorand the grant factories now take?DateTimeImmutable.readonlyclasses, so subclasses that add state failed to compile. They keep read-only properties instead.AccessTokenAuthenticatorbuilds its header fromgetAccessToken(), so an overridden getter is used.resolveAccessTokenRequest()dropped the PKCE verifier. The verifier is now added to whichever request is resolved.max_age=0, and could not replace standard parameters. They now match Saloon. Empty scopes are dropped and a"0"scope is kept, in authorization URLs and client credentials token requests alike.Mocking and fixtures
/usernever matched. Patterns now match the end of the URL, with or without its query string.assertSent()closures with amixed,objector intersection parameter threw. They now use Saloon's type matching.Set-Cookie, were stored unredacted. Rules now apply to each value.Saloon::fake()replaced the global mock client and discarded earlier fakes and recorded responses. It now adds to the existing client, as in Saloon; passing aMockClientstill replaces it.MockClient::withoutCache()keeps responses that reach the transport, such as a recorded fixture, out of the response cache.Pools
toException()treats as failed now reaches the exception handler, orPoolException::failures()when there is none.PoolExceptionhad no previous exception unless scheduling failed. It now chains the failure that stopped the pool, otherwise the first request or callback failure.Framework integration
Event::fake()andEvent::fakeFor()did not reach Saloon or the HTTP client once they were resolved, because both keep the dispatcher they were built with. Both service providers now pass a reboundeventsservice to the resolved service, so fakes apply and are removed afterwards. Both Saloon request events useDispatchable.integrations_namespacealways producedApp\Http\Integrations, whateverintegrations_pathsaid. It now follows the path beneath the app directory, after resolving.and..segments, and a path outside it needs a namespace.saloon:listgarbled endpoints that used interpolation, concatenation, dots inside strings or escaped quotes, and did not read double-quoted base URLs.make()parameters and constructor injection and threwArgumentCountErrorfor any request with constructor arguments.Requestnow implementsTransient, so it is fresh on every resolution and still gets its dependencies.Caching
failed()are cached now.CachedResponse, so every read missed. The service provider now allows it.clearCache()on the manager, connectors andHasCachingforgets a cached response without sending the request, using the same store, key and scope assend().CacheKeyis no longerfinal. It hashes withsha256because the key includes credentials, so a forged collision would return another caller's response.Pagination
Paginator::current().getBodyChecksum()is back, so APIs that add a per-request value to each page can compare the page contents only.Rate limiting
CalendarWindowis a new rate limiter policy that resets at the start of each minute, hour, day (at an optional time) or month, in the app's timezone or a given one. It follows the local clock across daylight saving changes. Saloon's end-of-minute, end-of-hour, midnight and end-of-month limits map to it. The Redis store computes the window from Redis's own clock and rejects a stale window, so a worker whose clock is in another period cannot charge the wrong one.Retry-Afterheader now records Saloon's 60-second cooldown. A zero or past value records none.waitForRateLimits()receives the denied policy and its decision, so a resource can wait out a burst limit and still throw for a daily quota. Waiting out a 429 cooldown with a retry policy sends again through the normal retry loop.resolveRateLimiterName()lets resources share a quota and its cooldown.Saloon::inspectRateLimit()checks a policy or cooldown without using it up, andrateLimitingEnabled()turns limits off for one operation.RateLimitReachedExceptionnames the limiter.API cleanup
SaloonManager::sender(),cache()andrateLimiter(), and the pending request'scache()andrateLimiter(), are removed. They only returned the injected services, nothing used them, and the framework's own facades and contracts provide the same services.PipeOrder's cases use Saloon'sFIRSTandLASTnames again.hypervel/reflection, which it never used. It now requiresguzzlehttp/uri-templatedirectly, andsymfony/filesystemfor the generators' path handling.Tests
tests/Saloonnow includes the test suites of Saloon and its four plugins, alongside the Hypervel-specific cases. Tests that call Saloon's live test API use the engine test server intests/Integration/Saloon. The rate limiter's shared store contract covers calendar windows on every store.Documentation
The Saloon guide now covers the whole package: request bodies, authentication, OAuth, middleware, error handling, retries, debugging, pools, caching, pagination, rate limits, plugins, events and testing. A wrong
shouldThrowRequestExceptionexample is fixed. The rate limiting guide documents calendar windows, and the Laravel porting guide gains a Saloon section. The package README lists the remaining differences from Saloon and its plugins, with the reason and the Hypervel equivalent for each.docs/upstream-sync/sync.yamlrecords the Saloon revisions the package matches and how each upstream repository maps onto it.Verification
The full parallel suite, PHPStan and formatting pass, and the Saloon and engine integration tests pass on the engine test servers. The rate limiter's store contract passes on the worker-array, Swoole, SQLite, MySQL, MariaDB, PostgreSQL and Redis stores.
Summary by cubic
Brings the Saloon package up to date with Saloon v4 and its Laravel, cache, pagination, and rate-limit plugins, fixing bugs that surfaced when running Saloon's own test suites against Hypervel's port. Behavior now aligns with Saloon where it diverged: responses a failure hook rejects are no longer cached or counted as pool successes, OAuth and multipart requests match what Saloon sends, mock URL patterns match Saloon's docs, and rate limits can reset on the clock.
Behavior changes
scheme://endpoints count as absolute URLs, so paths likedocuments:batchGetare accepted and query merging no longer produces&&or a leading&.NotFoundException,TooManyRequestsException, and more),defaultRetryPolicy(), API versioning, timeouts, request-owned connectors, and URL template parameters./user; fixture redaction applies per header value;Saloon::fake()adds to the existing mock client instead of replacing it.make()parameters, andEvent::fake()reaches resolved Saloon and HTTP client services.BodyExceptioninstead of resending an empty body.Framework and rate limiter changes
Content-Typewithout a boundary so Guzzle adds it, and accepts a replaced event dispatcher.CalendarWindowrate-limit policy resets at the start of each minute, hour, day (at an optional time), or month, handling daylight saving changes; the Redis store computes windows from Redis's own clock and rejects stale ones.Retry-Afterheader records Saloon's 60-second cooldown.Written for commit 380709f. Summary will update on new commits.
Note
Sync Saloon correctness, caching, and rate-limit improvements
SaloonManager.sendnow checks the response cache before mock matching, skips recording cache hits as mock sends, and applies a defaultRetryPolicywhen the request has none (SaloonManager.php)CalendarWindowrate-limit policy with Redis-time-based window validation and retry for stale boundaries (CalendarWindow.php, RedisStore.php)Response::toExceptionnow maps common 4xx/5xx statuses to dedicated exception classes likeTooManyRequestsExceptionandInternalServerErrorException; pool workers callthrow()so HTTP errors reach the pool exception handlerPoolExceptionpicks the right predecessor, and fixture path traversal is rejectedPendingRequestlosescache()/rateLimiter()accessors and constructor deps;SaloonManagerlosessender(),cache(),rateLimiter()accessors;BasicAuthenticatoruses an Authorization header instead of transport auth;AccessTokenAuthenticatorrequiresDateTimeImmutableexpiry;RequestdropsnewInstance()Macroscope summarized 380709f.