Search by

hypervel / saloon

albertchtbinaryfire

Build API integrations and SDKs for Hypervel.

Package info

github.com/hypervel/saloon

Issues

pkg:composer/hypervel/saloon

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

0.4.x-dev 2026-10-25 16:18 UTC

This package is auto-updated.

Last update: 2026-10-27 07:31:32 UTC


README

Documentation: https://hypervel.org/docs/saloon

Differences From Saloon

Requests and pending requests use fluent methods in the style of Hypervel's HTTP client, such as withHeaders, withoutHeader, withQueryParameters, withOptions and delay, and their getters return plain values. Upstream's ArrayStore and IntegerStore objects (headers()->add(), query()->remove(), delay()->set()) are not available: headers() returns an array, queryParameters() and delayMilliseconds() replace the query() and delay() getters, and upstream's config() and defaultConfig() are options() and defaultOptions(). Getters drop upstream's get prefix, so requests have method() and mockClient(), and pending requests have connector(), request(), method() and fakeResponse(); compare fakeResponse() with null in place of hasFakeResponse(). Pending requests change the method and URL with withMethod and withUrl instead of setMethod and setUrl, uri() replaces getUrl(), including the query string, and resolveResponseClass($response) returns the custom response class, or null for the default, in place of getResponseClass().

Connectors are read-only. Their headers, query parameters, options, body, authenticator and delay come from their default* methods and constructor values, because a connector may be shared by concurrent requests. Apply per-operation values to the request, or change connector defaults on the pending request in a hook or middleware. Connectors have no middleware() or debugging methods; register middleware and debuggers on the pending request from boot() or a plugin. For the same reason, requests and connectors allow absolute endpoints by overriding allowsBaseUrlOverride(); upstream's public $allowBaseUrlOverride property has no effect. HasApiVersion has no setApiVersion(): assign $apiVersion in the constructor or override getApiVersion().

There is no process-global Config class and no pluggable sender. Global middleware is Saloon::middleware(). Requests are sent through Hypervel's HTTP client on a named connection, saloon.connection.name or the connector's resolveHttpConnection(), so sender(), defaultSender(), sender resolvers, handler stacks and the sender's factory collection are not available; use the connection's options, Http::globalMiddleware(), or Saloon's PSR request hooks and middleware. Body repositories' toStream() and fake responses' createPsrResponse() take no PSR factories. Telescope's HTTP client watcher records Saloon requests with the saloon tag in place of the plugin's Telescope middleware, so fakes and cache hits are not recorded. The plugin's Pulse and Nightwatch middleware are not included, since Hypervel has neither package; OpenTelemetry's HTTP client instrumentation traces Saloon requests like other HTTP client requests. The plugin's ide.json code-generation metadata for the Laravel Idea PhpStorm plugin is not included; generate classes with the saloon:* commands (see Generating Integrations). Prevent stray requests with Http::preventStrayRequests(), which throws the HTTP client's StrayRequestException, including for a request that would record a missing fixture, so allow the API's URLs with Http::allowStrayRequests() while recording. Fake sleeping with Sleep::fake(), and control the clock through the framework Date clock.

createPendingRequest() takes no mock client. Mock clients are chosen and matched when the request is sent, after request middleware has run and the URL is final, so middleware does not see a mock response and pending requests have no hasMockClient() or getMockClient().

Connectors are read-only, so they have no withMockClient(): attach a mock client to the request, pass it to send(), or use Saloon::fake(), which also backs MockClient::global(). Mock clients use the HTTP client's names. recorded() returns the recorded responses as a collection, optionally filtered by a callback, in place of getRecordedResponses(), findResponseByRequest() and findResponseByRequestUrl(); lastRequest(), lastPendingRequest() and lastResponse() drop the get prefix; and match() replaces guessNextResponse() and getNextFromSequence(). The deprecated assertSentJson() is not included. Saloon::fake() also accepts a MockClient to install as the global client, and Saloon::mockClient() returns null until a fake is active instead of creating one. The plugin's deprecated response recorder (record(), stopRecording(), isRecording(), recordResponse(), getRecordedResponses() and getLastRecordedResponse()) and its MockClient subclass are not included: recorded() lists the responses recorded by the active mock client, and the SentSaloonRequest event observes every sent response. There is no MockConfig: fixtures live under the saloon.fixtures.path configuration, which tests override with Saloon::fixturePath() and Saloon::throwOnMissingFixtures(), and are read and written through the framework Filesystem rather than upstream's Storage helper. getFixturePath() returns the absolute path, fixture names separate directories with forward slashes, a missing directory is created when a fixture is first recorded, and recorded fixtures list every header value. See Testing.

Middleware contracts are typed: RequestMiddleware::__invoke() returns ?FakeResponse and ResponseMiddleware::__invoke() returns ?Response, so a ported middleware class declared : void returns null instead. A pending request returned from request middleware is ignored; change the one it receives. Pipelines expose pipes(), requestPipeline(), responsePipeline() and fatalPipeline() instead of upstream's get methods. Connector middleware is registered from boot() and runs after global and plugin middleware but before the request's own middleware.

Retries use retry($times, $sleepMilliseconds, $when, $throw) and a defaultRetryPolicy() method on requests and connectors instead of upstream's $tries, $retryInterval, $throwOnMaxTries and $useExponentialBackoff properties, handleRetry() methods and send()'s $handleRetry argument; use a delay closure for exponential backoff. A request's policy replaces the connector's entirely, so the two when conditions are not combined. The when callback receives the failed attempt's pending request; change $pendingRequest->request() to affect the next attempt. Fewer than one attempt or a negative interval throws instead of being raised to the minimum. Upstream's deprecated sendAndRetry() is not available. Delays apply to mocked responses as upstream, but not to cache hits. See Retries.

Pools send requests concurrently through coroutines instead of promises, so there is no sendAsync(), pending requests have no isAsynchronous() or setAsynchronous(), and pools accept requests but not promises. A pool's send() waits for every request and returns the successful responses keyed and ordered as the requests were given; process() sends them without keeping the responses. Concurrency is a positive integer: upstream's callable and unlimited 0 forms are not supported, because the coroutine pool has a fixed number of slots. Handlers receive the response or exception and the request's key, without upstream's aggregate promise. Ending a request generator stops further requests, but a request already waiting for a slot still starts and running requests are not canceled. Every response that toException() treats as failed, including through custom failure hooks, goes to the exception handler; upstream's real asynchronous path checks them only after Guzzle rejects a transfer. Unhandled request failures, handler failures and invalid items throw PoolException once every started request has finished, instead of being ignored or ending the pool early. requests() replaces getRequests() and calls a request callback each time the pool is sent, so a callback that returns a new iterable lets the pool be sent again. See Concurrent Requests.

Caching uses Hypervel's cache stores. Cacheable declares cacheFor() and cacheStore() instead of upstream's resolveCacheDriver() and cacheExpiryInSeconds(), so there is no Driver contract and no Flysystem, PSR or Laravel driver; add other caches as framework cache stores. CachingException replaces HasCachingException. Connectors are read-only, so they implement Cacheable without the HasCaching request controls and override cacheKey() and cacheableMethods(), which replaces getCacheableMethods(). GET, HEAD, OPTIONS and QUERY requests are cacheable by default. There is no CacheKeyHelper: a custom cacheKey() replaces the default request identity, and Saloon::resolveCacheScopeUsing() adds a scope, such as the tenant, to every key. Cached entries are CachedResponse values holding the status, headers and body, and they expire through the cache store, so CacheMiddleware, CacheRecorderMiddleware, hasExpired() and getFakeResponse() are not included. See Caching.

Paginators set their values with maxPages(), perPageLimit() and startPage(), and totalResults(), request() and currentPage() replace getTotalResults(), getOriginalRequest() and getCurrentPage(). The deprecated getPage() and $page are not included; custom applyPagination() methods read the protected $pageNumber. Page and offset paginators send their pages concurrently with pool(), which sends the first page, then the rest through a coroutine pool, and returns the responses keyed by page position, so there is no async(), isAsyncPaginationEnabled() or HasAsyncPagination and no promises to iterate. Cursor paginators can't be pooled because each cursor comes from the previous response. Iterator keys and currentPage() count from zero at the start page, and maxPages() counts pages from the start page. Upstream starts keys at startPage - 1 and compares maxPages with page numbers, so startPage(3) with maxPages(2) returns no pages there. See API Pagination.

Rate limits use Hypervel's rate limiter. resolveRateLimits() returns framework admission policies for the pending request instead of upstream's resolveLimits() and getLimits() with Limit and Bucket objects, and state lives in the rate-limiter store chosen by resolveRateLimitStore() or saloon.rate_limiter.store, so there is no RateLimitStore contract, rateLimitStore() or plugin store. LimitException is not included; invalid policies and store state throw the rate limiter's own exceptions. Limit::perMinute() and the other Limit factories replace upstream's every*() intervals, LeakyBucket replaces Bucket, and CalendarWindow replaces untilEndOfMinute(), untilEndOfHour(), untilMidnightTonight(), everyDayUntil() and untilEndOfMonth(). Set a lower limit in place of Limit::allow()'s threshold. The rate limiter's Redis, Swoole, database and worker-array stores replace upstream's stores. There are no file, Laravel cache or PSR cache stores, since a general cache can't apply these policies atomically, and no Predis store, since the Redis store uses the framework's PhpRedis connections. Identical policies share one state and each charges it, rather than throwing upstream's duplicate-name exception. waitForRateLimits($policy, $result) replaces sleep() on each limit and the handleExceededLimit() and throwLimitException() hooks, resolveRateLimiterName() replaces getLimiterPrefix() (use by() keys in place of name() for per-user limits), and Saloon fakes and cache hits consume no capacity. A 429 records a cooldown, 60 seconds when Retry-After is missing or invalid, through resolveRateLimitCooldown() in place of handleTooManyAttempts(), getTooManyAttemptsLimiter(), Limit::custom(), RetryAfterHelper and $detectTooManyAttempts; override it to read other responses, or return null to record nothing. The 429 goes through normal response and error handling, and an active cooldown applies to later requests. Nothing is resent automatically, so combine waitForRateLimits() with a retry policy to wait and send again.

Connectors are read-only, so rateLimitingEnabled(PendingRequest) replaces useRateLimitPlugin() and $rateLimitingEnabled, and Saloon::inspectRateLimit() replaces hasReachedRateLimit() and getExceededLimit(); compare remaining() with limit() in place of a threshold. RateLimitReachedException exposes policy() and result() instead of getLimit(), and the job middleware is Queue\ReleaseOnRateLimit instead of Helpers\ApiRateLimited. See Rate Limiting.

A custom response resolver receives the base response, so override resolveResponseClass(Response $response) instead of upstream's method without arguments.

Responses extend Hypervel's HTTP client response and keep its methods. Their getters drop upstream's get prefix (pendingRequest(), connector(), request(), fakeResponse()), and toPsrRequest() and toPsrResponse() replace getPsrRequest() and getPsrResponse(). fromResponse() creates a response from an HTTP client response in place of fromPsrResponse(), and response classes override the HTTP client's newRequestException() in place of createException(). withBufferedBody() is not included, since body() buffers a stream that can't be rewound, and neither is getSenderException(), since an error status never throws in the transport. header() returns the header line, an empty string when the header is missing. headers() lists every value. json() returns null for an empty body and uses the HTTP client's configurable decoding flags instead of JSON_THROW_ON_ERROR. object() takes decoding flags instead of a key, so use data_get($response->object(), $key) for keyed access. xmlReader() is not included: it only wraps XML Wrangler, whose collection helpers need Laravel's collections and whose dependencies need the xsl and bcmath extensions. Use xml() or dom(), or install XML Wrangler and call XmlReader::fromPsrResponse($response->toPsrResponse()).

Request exceptions extend the HTTP client's RequestException, so they use its message format and constructor, and expose response(), pendingRequest(), status() and body() instead of upstream's getters; use response()->reason() in place of getStatusMessage(). They are not SaloonExceptions, so catch RequestException for failed responses. FatalRequestException has pendingRequest() in place of getPendingRequest(). getRequestException(Response $response) must return a Saloon request exception and receives no sender exception; override an exception's prepareMessage() to change its message. A transfer that fails after the response headers arrive throws FatalRequestException whatever its status, rather than returning the partial response. Debugging with die: true exits, which throws Swoole's ExitException inside a coroutine, so there is no Debugger::$dieHandler.

Every request supports a body, so there is no HasBody contract to implement. The body traits set the default body, body() returns its value rather than a repository, and withBody, asJson, asForm, asMultipart, attach and withData change it in place of setBody(); a custom repository comes from defaultBodyRepository(). ArrayBodyRepository is abstract, so use JsonBodyRepository or FormBodyRepository instead. MultipartValue also accepts booleans, null and arrays without a filename or headers, which Guzzle sends as fields, so withData can add fields to a multipart body. A multipart body's Content-Type is added when the body is prepared, after request middleware runs, so it carries the final body's boundary unless you set a content type that declares its own.

The HasTimeout plugin's getConnectTimeout and getRequestTimeout methods return null for an undeclared timeout instead of upstream's Config defaults, so existing options and the HTTP connection's configured timeouts stay in effect. See Request Options.

Authentication helpers use the HTTP client's names: withToken(), withBasicAuth() and withDigestAuth(). Upstream's deprecated withTokenAuth(), withQueryAuth(), withHeaderAuth() and withCertificateAuth() are not included; pass the authenticator to authenticate() instead. authenticator() replaces getAuthenticator(). Authenticator properties are read-only because an authenticator may be shared by concurrent requests; to change credentials, create a new authenticator and pass it to authenticate(). DigestAuthenticator and withDigestAuth() take no digest-type argument, since digest is the only type supported. Digest credentials and client certificates are transport settings read with transportAuthentication() and certificate(), not request options.

The upstream NTLM authenticator is omitted. Integrations requiring NTLM must supply their own authenticator and transport middleware.

OAuth 2 configuration is an immutable Data\OAuthConfig built from constructor arguments and read through public properties, replacing upstream's Helpers\OAuth2\OAuthConfig setters and getters, so connectors define defaultOAuthConfig() and per-call changes use the grant methods' requestModifier. invokeRequestModifier() is modify(). authorizationUrl() returns an AuthorizationUrl holding the URL and its state, instead of getAuthorizationUrl() storing the state on the shared connector for getState(). Additional authorization parameters cannot replace that state, and if getAccessToken() receives either state, both must be non-empty and equal. getUser() applies the configuration's request modifier before the per-call one, as the token methods do, and its request sends no form Content-Type. OAuthAuthenticator also requires getScopes(): ?array; AccessTokenAuthenticator and both grants' createOAuthAuthenticator() factories append an optional ?array $scopes = null argument. This preserves reported permissions during account renewal: null means unreported, while [] is explicitly empty. See OAuth 2.

Ported from: