UPGRADING.md
Guzzle 8 is a major release that raises the minimum PHP version, updates the Guzzle dependency stack, adopts stricter PSR-7 header and request method behavior, changes some network exception classification, and tightens validation for request options, protocols, transport settings, cookies, and native method signatures. It also adds generic PHPDoc types to async APIs for static analysis.
Guzzle 8 requires PHP ^7.4 || ^8.0. Guzzle 7 supported PHP
^7.2.5 || ^8.0.
Guzzle 8 also requires Guzzle Promises 3.x and Guzzle PSR-7 3.x. If your application pins either package directly, update those constraints to allow the new major versions.
Read the Guzzle PSR-7 3.x upgrade guide as part of every Guzzle 8 upgrade. Guzzle's normal request and response APIs use Guzzle PSR-7, so PSR-7 changes can affect applications even when they do not instantiate PSR-7 classes directly. This guide calls out the most common inherited PSR-7 changes, but it does not repeat every PSR-7 behavior change.
Pay particular attention to that PSR-7 upgrade guide if your application constructs requests, modifies responses, sets headers, uploads files, builds multipart requests, works with URIs, or inspects streams.
Also read the Guzzle Promises 3.x upgrade guide if you use async requests, custom handlers, middleware, pools, or promise helpers.
Guzzle 8 now requires psr/http-factory:^1.0 directly.
Guzzle PSR-7 3.x serializes IPv6 hosts in native URIs in their RFC 5952
canonical form, and its UriComparator::isCrossOrigin() canonicalizes
bracketed IPv6 literals from any PSR-7 implementation before comparing
origins. Redirects between equivalent spellings of one IPv6 address are
therefore same-origin in Guzzle 8: Authorization and Cookie headers, the
auth option, cURL authentication options, and the same-scheme Referer path
and query may be retained where Guzzle 7 stripped or reduced them, and absolute
Digest domain protection spaces can cover an equivalent spelling of the same
address.
Guzzle 8 also keys its own host-scoped state, the Digest challenge cache and
the cookie jar, on the canonical IPv6 form, so equivalent spellings share one
entry instead of fragmenting state. A cached Digest challenge may be reused
preemptively across spellings instead of causing another 401 probe. Host-only
cookies extracted from one spelling match requests using another, persisted
cookie domain text is canonical, and equivalent cookie entries coalesce. Bare
IPv6 cookie domains, which the cookie API permissively accepts, canonicalize
without gaining brackets, and bare and bracketed forms remain distinct
identities. Cookie-domain matching remains exact-only for IP literals and IP
addresses, and DNS suffix matching now requires both the cookie domain and the
request host to be valid non-literal, nonnumeric host names, so curl-style
hexadecimal IPv4 forms such as 0x7f000001 stay exact-only.
Configured proxy endpoints are not canonicalized, no-proxy matching was
already representation-independent, and the URI text sent to the transport is
not rewritten: a foreign UriInterface that reports a noncanonical IPv6
spelling is serialized as supplied. IPvFuture literals, zone-bearing values,
and invalid bracketed text keep their ASCII-case-folded textual identity in
these Digest and cookie comparisons.
Guzzle 8 uses Guzzle PSR-7 3.x, and several of its behavior changes surface through normal Guzzle client usage. This section summarizes the inherited PSR-7 changes most likely to affect Guzzle users; read the Guzzle PSR-7 3.x upgrade guide for the complete list.
Header values passed through the headers request option or PSR-7 request APIs
must now be strings or non-empty arrays of strings. Empty strings remain valid
explicit header values, but empty arrays, null, false, integers, floats, and
other non-string values are no longer cast or accepted.
If your application builds headers from configuration, user input, or typed domain values, normalize them before creating or sending requests:
// 7.x, no longer accepted in 8.0
$client->request('GET', '/', [
'headers' => ['Api-Version' => 1],
]);
// 8.0
$client->request('GET', '/', [
'headers' => ['Api-Version' => '1'],
]);
Guzzle 8 preserves explicitly provided request method casing. HTTP method names
are case-sensitive, so Guzzle now sends the method exactly as provided and
applies built-in method-specific behavior only to exact standard method names
such as GET, HEAD, POST, and PUT.
If you previously relied on new Request('get', ...) or
$client->request('get', ...) being sent or treated as GET, normalize the
method before constructing or sending the request:
$client->request('GET', 'https://example.com');
The convenience methods such as $client->get(), $client->post(), and their
async variants continue to use uppercase standard methods.
Digest authentication is no longer implemented with cURL CURLOPT_HTTPAUTH and
CURLOPT_USERPWD. Guzzle supports legacy non-session Digest challenges without
qop and challenges with qop=auth; session algorithms require qop.
auth-int is not supported. To use libcurl's native Digest implementation
instead, omit auth and configure cURL options directly with a cURL handler,
including CURLOPT_HTTPAUTH => CURLAUTH_DIGEST and CURLOPT_USERPWD.
Guzzle 8 no longer treats ntlm as a built-in auth type. If an existing
NTLM-only integration must be kept temporarily, configure cURL options directly
with a cURL handler and a libcurl build that still supports NTLM:
$client->request('GET', '/', [
'curl' => [
CURLOPT_HTTPAUTH => CURLAUTH_NTLM,
CURLOPT_USERPWD => 'username:password',
],
]);
This is not a long-term migration path. curl/libcurl has deprecated NTLM because it is weak, deprecated by Microsoft, and does not work over HTTP/2 or HTTP/3. curl made NTLM opt-in in curl 8.20.0 and plans to remove support in September 2026.
Digest requests that carry a body send the initial unauthenticated probe with an
empty body and Content-Length: 0, as libcurl does. The body is sent on
authenticated attempts: once in the normal one-challenge handshake, and again if
a stale-nonce challenge must be retried. Because of this, non-seekable request
bodies work with Digest authentication unless a stale-nonce retry forces a
rewind. Followed redirects also rewind the body under the normal redirect rules,
independently of Digest. If the server answers the probe with anything other
than a 401 response, a 407 proxy challenge, or a followable redirect, the request
fails with a GuzzleHttp\Exception\ResponseException carrying that response,
because the probe did not represent the original request; remove the auth
option for endpoints that do not require authentication. See "Differences from
libcurl's Digest implementation" below.
Guzzle 8's Digest middleware deliberately diverges from libcurl, which
implemented the auth option's Digest support in Guzzle 7, in the following
ways:
GuzzleHttp\Exception\ResponseException with that response attached. This
exception pre-empts the ClientException or ServerException that
http_errors would otherwise raise for such a response; catch (RequestException $e) catches both. For unchallenged sub-300 probe responses
libcurl instead re-issues the request unauthenticated, which can cause the
server to process both the empty probe and the replay; for other statuses it
surfaces the probe response. If a server does not require authentication,
remove the auth option.Expect, Transfer-Encoding, Trailer, Content-Range,
Content-Encoding, Content-MD5, Digest, Content-Digest, and
Repr-Digest, and forces Content-Length: 0. Content-Type is kept, as
libcurl does. libcurl's probe is bodyless but not header-identical: it keeps a
caller-supplied Expect, keeps HTTP/1.1 Transfer-Encoding, sending a
chunked probe with a zero chunk and no Content-Length, and forwards other
caller headers untouched. Guzzle's stripping is deliberately stricter.auth-int is rejected. Challenges offering only qop=auth-int are
ignored. libcurl selects auth-int when auth is absent but hashes an empty
entity body, providing no actual body integrity.WWW-Authenticate header field and ignores later Digest fields. Multiple
Digest challenges inside one field are parser-dependent in curl.WWW-Authenticate header fields and
in some same-field shapes. The 401 is returned untouched only when no usable
Digest challenge remains. libcurl is lenient: later duplicate values overwrite
earlier ones, stale and userhash flags are sticky once set, and malformed
tails are tolerated after enough valid material has been parsed.domain parameter when scoping reuse, and curl's Digest state has no domain
handling. Guzzle generates a fresh cnonce per Authorization header, where
curl's non-SSPI implementation reuses one cnonce per nonce while incrementing
nc. When domain is absent the cached challenge covers the whole origin, so
on origins that host multiple realms or trust boundaries a preemptive request
can disclose the username or userhash and password-derived response material
for one realm to another same-origin endpoint before it challenges; use
separate origins, a server-side domain, or disable reuse by replacing the
default auth middleware with GuzzleHttp\Middleware::auth(false) as shown in
the auth request option documentation.on_stats, on_headers, and progress fire per leg, and delay applies
once, before the first Digest leg: the probe, or a preemptive request when
challenge reuse applies. libcurl performed the whole handshake inside one
transfer with one stats callback.stream => true plus a configured sink, a Digest
request drains the final body into the sink, protecting sinks from challenge
bodies on handlers that ignore stream. A non-digest request on the stream
handler leaves the sink untouched.CURLOPT_PROXYAUTH,
and libcurl defaults proxy auth to Basic, so proxy Digest requires a custom
handler.Non-seekable request bodies work for the normal handshake because the probe never consumes them. A stale-nonce retry after the body has been consumed still requires a seekable body, the same limit libcurl has.
Built-in Basic and Digest authentication continue to work for clients using
Guzzle's default handler or GuzzleHttp\HandlerStack::create($handler), but
they are now applied by the default auth middleware instead of while the client
constructs the request. If you pass a raw custom handler directly to Client,
remove default middleware, or build a handler stack manually, Basic and Digest
authentication will only be applied when your stack includes
GuzzleHttp\Middleware::auth(). Unknown auth type strings are left in the
request options for custom middleware or custom handlers.
Guzzle also validates non-empty auth arrays before applying them: indexes 0
and 1 must be username and password strings, and index 2, when present, must
be a string or null. Invalid auth arrays that previously emitted warnings,
coerced values, or did nothing now throw
GuzzleHttp\Exception\InvalidArgumentException.
// Valid:
$client->request('GET', '/', [
'auth' => ['username', 'password', 'basic'],
]);
// Invalid in 8.0:
$client->request('GET', '/', [
'auth' => ['username'],
]);
Guzzle 8's built-in authentication middleware rejects Basic auth credentials
when the username contains a colon or either the username or password contains
an ASCII control character. Guzzle 7 encoded and sent these values. Colons
remain valid in passwords.
Guzzle 8 also no longer forwards the generic auth request option when
automatic redirects cross origin. Guzzle already removed the Authorization and
Cookie headers and cURL HTTP authentication options on cross-origin redirects;
this now also applies to handler-visible auth state.
Same-origin redirects continue to preserve auth. If an application or custom
handler intentionally reused auth across redirected origins, disable automatic
redirects or handle redirects manually so each origin receives explicit
credentials.
Some Guzzle 8.0 exception changes add intermediate classes, while others
intentionally reclassify specific failures. Broad TransferException and
GuzzleException catch blocks still catch all Guzzle transfer failures, and
RequestException still catches response-aware and other non-network request
failures. More specific catch blocks need auditing: Guzzle 8.0 splits timeout
failures into connect-phase, no-response network, and response-aware timeout
classes, and the built-in cURL and stream handlers classify more transport
failures by transfer phase. This section covers upgrading from Guzzle 7.x to
Guzzle 8.0. The ConnectException inheritance change happened earlier, in
Guzzle 7.0.0, when it moved out from under RequestException; see the 6.0 to
7.0 notes for that migration. The hierarchy changes are easiest to compare in
the two trees below.
Guzzle 7.x uses this hierarchy:
. \RuntimeException
└── TransferException (implements GuzzleException)
├── ConnectException (implements NetworkExceptionInterface)
└── RequestException (implements RequestExceptionInterface)
├── BadResponseException
│ ├── ClientException
│ └── ServerException
└── TooManyRedirectsException
Guzzle 8.0 uses this hierarchy:
. \RuntimeException
└── TransferException (implements GuzzleException)
├── HandlerClosedException
├── NetworkException (implements NetworkExceptionInterface)
│ ├── ConnectException
│ │ └── ConnectTimeoutException
│ └── NetworkTimeoutException
└── RequestException (implements RequestExceptionInterface)
└── ResponseException
├── BadResponseException
│ ├── ClientException
│ └── ServerException
├── ResponseTransferException
│ └── ResponseTimeoutException
└── TooManyRedirectsException
NetworkException is new in Guzzle 8.0 as the base class for no-response
network failures. Throughout Guzzle 7.x, ConnectException implements
Psr\Http\Client\NetworkExceptionInterface directly and there is no
Guzzle-specific network base class. Reusable packages that support both Guzzle
7.x and 8.0 should catch Psr\Http\Client\NetworkExceptionInterface for
no-response network failures. Applications or packages that require Guzzle 8.0
may catch GuzzleHttp\Exception\NetworkException for all no-response network
failures. Keep catching ConnectException only when handling connection
establishment failures specifically.
Guzzle 8.0 also makes response-aware request failures explicit. Guzzle 7.x does
not have ResponseException, and response-aware request failures remain under
RequestException throughout Guzzle 7.x. In Guzzle 8.0, ResponseException is
the base class for request failures where response headers were received and a
response object is available. ResponseTransferException is used for
transfer-level failures after headers, including response-aware network,
protocol, content-decoding, partial-body, and response-body transfer failures.
BadResponseException and TooManyRedirectsException also extend
ResponseException; custom subclasses of those existing classes, or of
ClientException and ServerException, must not override the now-final
ResponseException::__construct().
Only this branch exposes response access: RequestException no longer stores
responses, no longer accepts a response constructor argument, and no longer has
getResponse() or hasResponse() methods. Catch ResponseException, or test
with instanceof ResponseException, before calling getResponse().
Timeout exception classes are now split by the transport phase the handler can
determine. ConnectTimeoutException is thrown for detected connect timeouts
(DNS resolution, TCP connect, proxy CONNECT, or TLS handshake). It extends
ConnectException, so code that catches ConnectException will also catch
connect timeouts. NetworkTimeoutException is thrown for other detected
transport timeouts before response headers are received. It extends
NetworkException, but not ConnectException. ResponseTimeoutException is
thrown for response transfer timeouts after response headers are received. It
extends ResponseTransferException and exposes the response.
Timeouts that originate from caller-supplied PSR-7 streams are not transport
timeouts. Reading the request body stream, including size detection,
stringification, rewind, and upload reads, is a RequestException before a
response and a ResponseException after response headers. A slow response
sink write is a ResponseException once a response exists, or a
RequestException otherwise.
If a handler throws Error, TypeError, or another non-Exception Throwable
before returning a promise, Client::sendAsync() returns a rejected promise.
Waiting on it rethrows the original throwable. During request and response body
handling outside native cURL callbacks, Guzzle still wraps \Exception failures
as RequestException or ResponseException where appropriate, while
non-Exception throwables propagate unchanged. Native cURL callbacks remain
different. A throwable from a cURL sink write is wrapped as
ResponseException when a response was received, or RequestException
otherwise, and the cURL handlers continue to report CURLE_WRITE_ERROR as
on_stats handler error data.
HandlerClosedException is new in Guzzle 8.0. It extends TransferException
and is used when an explicitly closed CurlMultiHandler rejects transfers that
are still pending, including delayed transfers. Existing Guzzle 7.x code should
not normally see this exception just by upgrading. Guzzle 7.x did not have this
exception or a public cURL handler close() method, and destructor cleanup did
not reject pending promises. If you add deterministic cleanup with
CurlMultiHandler::close(), wait for or cancel outstanding transfers first, or
handle HandlerClosedException or TransferException for pending promises you
may still observe.
When updating catch blocks for Guzzle 8.0, catch the more specific no-response
and response-aware failures before RequestException:
use GuzzleHttp\Exception\NetworkException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ResponseException;
use GuzzleHttp\Exception\ResponseTransferException;
try {
$client->request('GET', $uri);
} catch (NetworkException $e) {
// No-response network failures, including reclassified cURL and stream
// handler failures.
} catch (ResponseTransferException $e) {
$response = $e->getResponse();
// Transfer-level failures after response headers were received.
} catch (ResponseException $e) {
$response = $e->getResponse();
// Other request failures after response headers were received.
} catch (RequestException $e) {
// Request failures where Guzzle does not expose a response object.
}
The built-in handlers also reclassify several failures that Guzzle 7.x reported
as RequestException or ConnectException:
ConnectException.NetworkException.ResponseTransferException. This includes response-aware cURL connection,
network, protocol, content-decoding, partial-body, and response body transfer
failures. Response-body network stalls are ResponseTimeoutException, while
sink write failures, progress callback failures, deterministic response
size/platform-limit failures, or request-body stalls after headers are plain
ResponseException instances.ResponseException. A seekable response sink that fails to rewind does not
become a ResponseTransferException. Non-seekable sinks are not rewound, and
source close cleanup after a complete stream-handler download is best effort.ResponseException.This applies to both the cURL and stream handlers; the exact error codes and messages each one maps onto these classes are an implementation detail.
The cURL handler no longer treats non-101 informational responses such as
100 Continue, 102 Processing, or 103 Early Hints as the final response. If
a cURL transfer fails after receiving only one of those interim responses,
Guzzle now reports a no-response failure such as NetworkException instead of a
ResponseTransferException carrying the interim 1xx response. Code that
previously caught this path with RequestException or
RequestExceptionInterface should catch NetworkExceptionInterface or
TransferException instead. 101 Switching Protocols is unchanged and is still
surfaced as a response.
The built-in cURL and stream handlers now reject a response subject to body
framing when its Content-Length is malformed, conflicting, or present with
Transfer-Encoding. Equivalent decimal duplicates remain valid, including
leading zeros. Framing failures detected from a complete header block occur
before on_headers sees that response or any of its body bytes are delivered,
and raise ResponseTransferException. A transport that rejects the block first
can instead report its existing transfer failure. When a handler needs the
length as a PHP integer, a valid value above PHP_INT_MAX raises plain
ResponseException with the underlying OverflowException available through
getPrevious(). The stream handler does not impose this platform limit on
stream => true responses. Framing validation does not apply to responses to
HEAD, any 1xx, 204, 304, or successful CONNECT. 205 Reset Content
remains subject to framing validation.
The stream handler also raises ResponseTransferException after draining a
non-streamed response when a valid, positive Content-Length declares more
bytes than it receives. For decoded gzip and deflate, the length applies to
encoded bytes before decompression, and the original values remain in
x-encoded-content-length. Valid stream => true responses remain lazy after
header validation.
For chunked responses through the stream handler, the transport resource's
wrapper_data can now retain the original Transfer-Encoding. This is visible
on streamed bodies and to custom stream factories. On PHP 8.1.20+, 8.2.7+, and
8.3+, progress also counts raw chunk framing coalesced with the response
headers. Later socket reads were already counted before decoding. Guzzle's
response continues to expose the decoded body without that consumed header.
The deprecated RequestException::wrapException() method was removed. Create a
RequestException directly for request failures where Guzzle does not expose a
response object. Its third constructor argument is now the exception code,
followed by the previous exception. For failures with a response, create
ResponseException, ResponseTransferException, ResponseTimeoutException,
BadResponseException, ClientException, ServerException, or
TooManyRedirectsException instead. If you used the removed
getHandlerContext() methods to classify failures, use the more granular
exception classes above instead, or use on_stats when you need handler timing
or statistics. GuzzleHttp\Exception\InvalidArgumentException remains outside
the transfer exception hierarchy and is still used for invalid configuration or
request option values that can be rejected before a transfer starts.
RequestException::create() no longer accepts a handler-context array. Its
fourth argument is now the optional BodySummarizerInterface; remove any
handler-context argument and use the new exception classes or on_stats when you
need transport details.
TransferException itself now also requires the failing request as its second
constructor argument, and its message argument is no longer optional;
getRequest() is available on the entire transfer exception hierarchy. The
subclasses' own constructors, such as RequestException and
ConnectException, already required a request in Guzzle 7, so only code
constructing the base class changes: replace new TransferException('msg')
with new TransferException('msg', $request).
Before sending a body, the built-in cURL and stream handlers now finalize the
request framing. Each Content-Length value must be a non-negative decimal
integer that the selected transport can represent. Multiple values are allowed
only when they agree. Guzzle emits a single canonical value, and it must match
the available body size when that size is known. A request cannot include both
Content-Length and Transfer-Encoding. The only request transfer coding
Guzzle supports is a single chunked value on HTTP/1.1. Guzzle removes that
marker and adds an exact length when one is available. Otherwise, it lets the
transport choose valid wire framing. Other codings, coding chains, repeated
chunked values, unknown-length HTTP/1.0 bodies, and chunked on other HTTP
versions are rejected.
A Transfer-Encoding: chunked header is only a provisional framing marker:
Guzzle treats the request body as content, never as pre-encoded chunks. Callers
that pre-encoded bodies for Guzzle 7's stream handler must remove the chunk
framing before upgrading, or the server receives it as literal content.
For an unknown-size body with an explicit Content-Length, the stream handler
treats that length as the body boundary and does not read beyond it. Both
handlers reject premature EOF and request-body streams that return more bytes
than requested. The stream handler captures an otherwise unknown body once and
sends it with its exact length. Violations raise RequestException before a
response, or plain ResponseException if a cURL request-body read fails after
response headers. Omit both framing headers and let Guzzle choose.
PrepareBodyMiddleware now adds a provisional Transfer-Encoding: chunked
marker only to unknown-size HTTP/1.1 bodies. For other protocol versions, custom
handlers receive no provisional framing header.
A retry that reuses a consumed non-seekable body now fails if its known
remaining size no longer matches an explicit Content-Length. Use a seekable or
otherwise repeatable body for requests that may be retried.
Guzzle 8 rejects additional malformed request option values at the client
boundary. force_ip_resolve must be v4 or v6; protocols and
allow_redirects.protocols may contain only http and https; and delay
must be finite and non-negative. Per-request cookies values must be false
or a CookieJarInterface; the true shorthand is only valid in the client
constructor.
The following 33 options are now validated against their documented types before a transfer starts.
The boolean flags http_errors, stream, and synchronous must be bool.
decode_content and verify accept bool or string, expect accepts
bool or int, and debug accepts bool or a stream resource.
allow_redirects accepts bool or an array in which max is an int;
strict, referer, and track_redirects are bool; on_redirect is a
callable; and protocols is a non-empty array of http and https strings.
crypto_method, crypto_method_max, and retries must be int.
connect_timeout, read_timeout, and timeout must be int or float,
delay must be a finite, non-negative int or float, and version accepts
string, int, or float. cert_type and ssl_key_type must be string.
cert and ssl_key accept a string path or a [path, password] array
whose optional password may be a string or null. auth accepts false, a
string, or a [username, password] array of strings with an optional third
element that may be a string or null.
headers must be an array whose values are strings or non-empty arrays of
strings. form_params must be an array of strings, numbers, booleans, null,
or nested arrays of the same, and its floats must be finite. multipart must
be an array of part arrays, each with a string or int name, a contents
entry, optional string header values, and an optional string filename.
protocols must be a non-empty array containing only http and https.
curl and stream_context must be arrays.
on_headers, on_stats, on_trailers, and progress must be callables.
sink accepts a string path, a stream resource, or a StreamInterface, and
per-request cookies values must be false or a CookieJarInterface as
described above. Invalid values throw
GuzzleHttp\Exception\InvalidArgumentException naming the option and the
expected types.
A few build- or version-specific rejections that previously threw
InvalidArgumentException now throw RequestException, matching how the
HTTP-version capability checks already behave: requesting crypto_method TLS
1.3 on a libcurl built without it, and a stream-handler proxy whose raw
transport this PHP build's stream_get_transports() does not provide (for
example tls:// on a build without OpenSSL). The same input works on another
build, so it is a request failure rather than a caller error; a scheme invalid
on every build (such as udp:// or ftp:// for a proxy) still throws
InvalidArgumentException. Only code catching the specific exception type for
these build-misses needs to change.
Guzzle 7 deprecated the handler request option with a warning that Guzzle 8
would ignore request-level handlers. Guzzle 8 rejects the option instead:
passing handler in per-request options throws
GuzzleHttp\Exception\InvalidArgumentException before a transfer starts.
Silently ignoring the option would fail open — a test that supplied a
per-request MockHandler would fall through to the client's real handler and
hit the network. Configure the handler when creating the client, or use a
separate client instance for requests that need a different handler:
// Guzzle 7 (deprecated, used the request-level handler)
$client->request('GET', '/status', ['handler' => $mockHandler]);
// Guzzle 8
$client = new Client(['handler' => HandlerStack::create($mockHandler)]);
$client->request('GET', '/status');
Guzzle's default http_errors middleware uses BodySummarizer to include a
short response body summary in response-aware exception messages. In Guzzle 7,
creating that summary rewound seekable response bodies to the beginning. In
Guzzle 8, BodySummarizer still summarizes from the beginning of the body, but
then restores the body cursor to its previous position.
Most applications do not need changes. Check only code that catches
response-aware exceptions from http_errors and then reads the response body
while relying on exception creation to leave that body rewound. If you need to
read the body from the beginning after catching the exception, call
Message::rewindBody() explicitly.
Invalid request protocol versions are no longer treated as omitted. Passing
'version' => '', 'version' => 'HTTP/1.1', or sending a PSR-7 request whose
protocol version is empty or malformed now fails before the request is sent.
Omit the version request option to use Guzzle's default HTTP/1.1 behavior, or
pass an explicit supported protocol version such as '1.1'. Do not include the
HTTP/ prefix.
Invalid version request option values are still rejected with
GuzzleHttp\Exception\InvalidArgumentException before a request is sent.
Empty or malformed protocol versions returned by a RequestInterface, and
well-formed but unsupported protocol versions such as HTTP/3 with the stream
handler, now fail with GuzzleHttp\Exception\RequestException. If you
previously caught InvalidArgumentException or ConnectException for request
protocol version failures, catch RequestException or GuzzleException
instead.
If you use the progress request option with the built-in cURL handlers, audit
callbacks for return values. Any truthy return value now aborts the transfer and
rejects the request with ResponseException when a response is available, or
RequestException otherwise. Return 0, false, or nothing to keep the
transfer running.
If a cURL progress callback throws, catch ResponseException for
response-aware failures or RequestException for broader request failures, and
inspect getPrevious() for the original throwable. The throwable no longer
escapes directly from the native cURL callback.
The stream handler still ignores progress return values.
Exceptions thrown by on_stats remain unwrapped, so existing catch logic for
on_stats exceptions does not need to change. The built-in cURL handlers now
release native easy handles before invoking on_stats. Built-in handlers now
reject non-callable on_stats values before starting the transfer, and the
built-in cURL handlers reject non-callable on_trailers values the same way.
Raw callbacks passed through the curl request option remain low-level cURL
callbacks and are not normalized by Guzzle.
The on_headers request option callback now receives the request as its second
argument. Existing userland callbacks that accept only the response continue to
work, but callbacks that inspect all arguments, for example with
func_get_args() or a variadic parameter, will observe the additional
Psr\Http\Message\RequestInterface argument.
The on_trailers request option callback now receives the request as its third
argument, after the trailer array and the response; callbacks that accept only
the first two arguments continue to work. Exceptions thrown by on_trailers
now reject the request with ResponseException instead of RequestException,
and the callback now runs before on_stats instead of after it, so on_stats
observes an on_trailers failure as the transfer's outcome.
When requests are sent through GuzzleHttp\Pool, the pool now appends the
request's iterable key as one extra trailing argument to any on_headers,
on_trailers, on_stats, progress, and allow_redirects.on_redirect
callbacks provided via its "options" configuration; callable requests yielded
to the pool likewise receive the wrapped callbacks in their options argument.
Existing userland callbacks that do not declare the extra parameter continue
to work unchanged, but callbacks that inspect all arguments, for example with
func_get_args() or a variadic parameter, will observe the additional
argument, and arity-strict PHP internal callables used directly may reject the
extra argument and should be wrapped in a userland callback. progress return
values are still honoured, and requests sent directly through a client are
unaffected.
The built-in handlers invoke on_headers for the final response headers and for
101 Switching Protocols, but not for other informational 1xx responses such
as 100 Continue or 103 Early Hints. Guzzle does not expose a separate Early
Hints API; a dedicated interim-response hook may be added in a future minor
release.
The stream handler never reads the response body of a HEAD request, of a 1xx,
204, or 304 response, or of a 2xx response to a CONNECT request. Such responses
now always carry an empty body created by the configured stream_factory; the
transport connection is closed as soon as the headers (and the on_headers
callback) complete successfully; the sink option is not opened or written
(string path sinks create no file); and stream => true yields the empty stream
rather than the live transport stream. In particular, stream => true no longer
exposes the live transport for a 2xx CONNECT response. Per RFC 9110 these
responses cannot carry content, so any bytes a misbehaving server sends after
the header section are never read. The cURL handler already behaved this way via
libcurl.
The multiplex request option defaults to Multiplexing::WAIT:
HTTP/2 requests on the cURL handlers set libcurl's CURLOPT_PIPEWAIT, so a
concurrent burst waits for an in-progress connection it may be able to share
instead of dialing one connection per request. Guzzle 7 leaves this to libcurl,
which never waits by default. Pass
'multiplex' => Multiplexing::EAGER to restore the old dialing
behaviour, or Multiplexing::REQUIRE_EAGER/Multiplexing::REQUIRE_WAIT to
fail loudly unless a multiplexed protocol is guaranteed.
HTTP/2 requests also now require libcurl 7.65.2 or newer, so waiting is never silently unavailable where HTTP/2 works.
PHP resources passed as the sink request option are no longer closed when the
response body is closed or garbage-collected by the built-in cURL and stream
handlers. Applications that relied on Guzzle closing a raw resource sink should
close the resource explicitly or pass a string path instead.
The stream handler now trims only the trailing CRLF pair from the serialized header block it passes to the PHP stream context. Guzzle 7 also removed other trailing whitespace there, which could drop trailing spaces or horizontal tabs from the final header value when a request came from a PSR-7 implementation that does not trim header values.
The built-in cURL and stream handlers now default HTTPS requests to TLS 1.2 or
newer. Applications that must connect to legacy TLS 1.0 or TLS 1.1 endpoints can
explicitly lower the minimum version with the crypto_method request option:
$client->request('GET', 'https://legacy.example.com', [
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT,
]);
Guzzle 8 requires libcurl 7.34.0 or higher with SSL/TLS support when using the built-in cURL handlers. If this requirement is not met, the default handler stack will not select cURL automatically, and manually configured cURL handlers reject requests.
GuzzleHttp\Handler\Proxy::wrapTlsFallback() has been removed. Custom handler
stacks no longer need it; the default handler stack selects the cURL or stream
handler based on this TLS support automatically.
Applications that manage built-in cURL handlers or factories directly should
treat closed instances as unusable. Reusing a closed CurlHandler,
CurlMultiHandler, or CurlFactory throws BadMethodCallException; create a
new instance instead.
If CurlMultiHandler::close() is called while transfers are pending, those
promises are rejected with GuzzleHttp\Exception\HandlerClosedException.
Destructor cleanup remains best-effort and does not reject pending promises.
A custom handle_factory passed to a built-in cURL handler remains
caller-owned. Closing the handler does not close an injected factory.
Built-in handlers validate timeout option values when they apply those options.
timeout is applied by both built-in transports. connect_timeout is applied
by cURL handlers and accepted without effect by the stream handler.
read_timeout is applied by the stream handler and accepted without effect by
cURL handlers. When a built-in handler applies a timeout option, positive
values below 0.001 seconds now throw InvalidArgumentException instead of
being converted to no timeout.
The stream handler now enforces the timeout request option as a total
transfer deadline when it buffers the response, matching the cURL handlers'
treatment of the option as the total time of the request. Once the deadline
passes while the response body is being buffered, the transfer is aborted and
the request is rejected with ResponseTimeoutException. Each body read is
bounded by the shorter of the read_timeout idle timeout and the remaining
total timeout. A response whose header block arrives in small pieces past the
deadline is rejected once the headers are complete, because PHP's HTTP stream
wrapper can only bound the time between packets while waiting for response
headers. In Guzzle 7, the stream handler applies timeout only while
connecting and as the idle time between packets, so a server that keeps
sending small pieces of data can extend a request past the configured timeout
indefinitely and still produce a successful response.
With the stream request option enabled, a response whose header block
completes past the deadline is rejected in the same way, and the response
carried by the exception has a closed body. The deadline does not apply to the
streamed response body, which is governed by the read_timeout idle timeout.
The stream handler now treats timeout and read_timeout as orthogonal
controls. timeout is the total deadline for completing a buffered request or
obtaining a streamed response, with no deadline by default. read_timeout is
an idle timeout bounding how long the connection may sit silent at any stage,
while connecting, waiting for response headers, and between reads on the body,
defaulting to 60 seconds; 0 disables it. Any received data resets the idle
clock; nothing resets the deadline.
In Guzzle 7, both behaviours fall back to the default_socket_timeout ini
setting (60 seconds by default), timeout also acts as the idle cap, and
read_timeout only governs streamed reads. Set read_timeout explicitly to
tune or disable idle protection; default_socket_timeout no longer has any
effect on the stream handler.
The built-in cURL handlers now default the connect timeout to 60 seconds
instead of libcurl's 300, and connect_timeout set to 0 now disables the
connect timeout instead of selecting the 300-second libcurl default, matching
how 0 disables the timeout and read_timeout options. The stream handler
still accepts connect_timeout without effect; its connect phase is bounded
by the read_timeout idle timeout, which shares the 60-second default.
The stream handler no longer lets PHP's HTTP stream wrapper inject header
values from ini configuration. A request without a User-Agent header no longer
sends the user_agent ini value, and the from ini value is no longer sent:
the wrapper offers no way to omit the From header entirely, so deployments
that configure the ini send an empty From header instead of the configured
address. Both now match the cURL handlers, which ignore those ini settings.
Set the headers explicitly on the request or client to send them.
The proxy request option is validated more strictly. Proxy values must be
strings, and the proxy['no'] value may be either an array of strings or a
comma- or whitespace-delimited string such as the value from the NO_PROXY
environment variable. Other values now throw InvalidArgumentException.
Guzzle 7 skips invalid no entries instead of rejecting them.
When a request uses HTTP/3 and a proxy is resolved from the environment, the
request is now downgraded to HTTP/2 or HTTP/1.1 in the same way as for proxies
configured through the proxy request option, since current libcurl releases
cannot carry HTTP/3 over an HTTP proxy. A request excluded by the environment
no_proxy list stays direct and keeps HTTP/3.
No-proxy lists are interpreted identically everywhere they appear — the option's
no list, the client-mapped NO_PROXY environment variable, and the
environment no_proxy consulted by the built-in handlers — and the shared
interpretation follows libcurl's. Compared to Guzzle 7, this changes how string
lists are tokenized, how leading-dot entries match, and what a match means.
String no lists are split on whitespace as well as commas, and a leading-dot
entry such as .example.com now matches example.com as well as its
subdomains, exactly like the bare example.com entry. Both changes align the
option's matching with libcurl's interpretation of no_proxy, which the
environment path already followed. In Guzzle 7, the client mapping removes
spaces from NO_PROXY values, while the option form treats space-joined
values as a single entry that never matches and limits leading-dot entries to
subdomains.
A matching no entry now applies even when the array does not configure a proxy
for the request scheme: the request goes direct, and the built-in handlers do
not fall back to an environment proxy for it. The no list is also validated in
that case. Guzzle 7 ignores the no list entirely unless the array selects a
proxy for the request scheme.
The stream handler now throws InvalidArgumentException for any proxy URL
whose scheme it cannot execute: https://, SOCKS (socks4://, socks4a://,
socks5://, socks5h://), and anything other than http:// or a raw PHP
transport such as tcp://, ssl://, or tls://. Guzzle 7 passed these to
PHP, which failed later with an "unable to find the socket transport" error.
The cURL handlers reject a proxy URL whose scheme libcurl cannot use as a
proxy with the same exception. Raw PHP transport values still pass through the
stream wrapper unchanged.
Both handlers also reject a malformed proxy URL (an invalid host, an
out-of-range port, or leading junk before the scheme) up front with the same
InvalidArgumentException, instead of passing it to libcurl or PHP to fail on
later.
An http:// or scheme-less proxy that omits the port now defaults to 1080,
libcurl's default HTTP proxy port, so proxy.example.com resolves to
tcp://proxy.example.com:1080. Guzzle 7 passed a port-less proxy through
unchanged, which PHP's stream wrapper could not use because it requires an
explicit port.
Requests tunneled through an HTTP proxy, meaning an https:// target sent
through an http:// or https:// proxy, or a tunnel forced with raw curl
options, now require libcurl 7.54.0 or newer. HTTPS proxies (an https://
proxy URL, where the connection to the proxy itself is encrypted) also now
require libcurl 7.54.0 built with HTTPS-proxy support, raised from the 7.52.0
floor used in Guzzle 7. The built-in cURL handlers reject such requests on
older libcurl with a RequestException before they are sent, matching the
other build- and version-specific capability checks.
On supported libcurl, the proxy's CONNECT reply is suppressed with
CURLOPT_SUPPRESS_CONNECT_HEADERS. Guzzle 7 delivered the interim
200 Connection established header block to the on_headers callback and
could attach it to exceptions as a response. In Guzzle 8, on_headers
observes only origin responses, and a tunneled transfer failure is classified
by its transport phase instead of as a response failure carrying the proxy's
interim reply.
In Guzzle 8, every first-class Proxy-Authorization field handled by a built-in
cURL handler requires libcurl 7.37.0 or newer with proxy header separation
support, regardless of Guzzle's predicted route. This includes an empty field,
which keeps its cURL header-control meaning in the proxy-only list, and requests
predicted to be direct, no-proxy-bypassed, or sent through SOCKS. A request is
rejected with a RequestException before network I/O when separation is not
available. Guzzle 7 instead omits the field on those known non-HTTP-proxy routes
and requires separation only when it cannot safely discard the field.
Guzzle 8 also rejects a raw CURLOPT_PROXYHEADER list when proxy header
separation is unavailable. Guzzle 7.15 only deprecates the raw option and passes
it through to the installed PHP cURL extension and libcurl.
When the stream handler selects a proxy, Guzzle 8 rejects any first-class
Proxy-Authorization field, including an empty value, because PHP streams have
no proxy-only header channel. Guzzle 7 instead accepts exactly one value,
rejects carriage returns and line feeds, and adds one canonical proxy
authorization line after selecting the proxy. That first-class value, including
an empty one, takes precedence over Basic credentials in proxy URL userinfo;
multiple values are rejected. Direct and no-proxy-bypassed stream requests omit
the field in both versions. Configure Basic proxy credentials in the proxy URI
or use a cURL handler for a first-class proxy authorization field.
From libcurl 7.57.0, a cURL share handle passed directly to CurlFactory and
Guzzle's worker-global persistent sharing pools are treated as opaque
connection caches: anonymous HTTP and HTTPS proxy tunnels are forced onto
fresh, non-reusable connections, and TransportSharing::PERSISTENT_REQUIRE
rejects them. Guzzle 7 applies the same policy to its configured share handles
and additionally rejects the deprecated request-level CURLOPT_SHARE option
for proxy tunnels there; Guzzle 8 rejects that raw option outright, so only
the configured and persistent surfaces exist. Handler-lifetime
transport_sharing keeps ordinary tunnel reuse because its shares never lock
connections.
The stream handler now resolves proxies from the environment the same way the
cURL handlers do. When the proxy request option makes no decision for a
request, it reads http_proxy (lowercase only), https_proxy/HTTPS_PROXY,
and all_proxy/ALL_PROXY, and consults no_proxy/NO_PROXY — including *
to disable proxying entirely. The uppercase HTTP_PROXY is never read (an
httpoxy defense), empty values are treated as unset, and on Windows proxy
environment variables are resolved only under the CLI SAPI. Guzzle 7's stream
handler never consulted the environment; it honored only the explicit proxy
option.
Because the stream handler cannot tunnel or speak TLS to a proxy, an
https_proxy or all_proxy set to an https:// or SOCKS URL now throws
InvalidArgumentException, exactly as the same value passed through the proxy
option already does, instead of connecting directly. An environment http://
proxy used for an "https" request still fails at connect time, because the
handler cannot open a CONNECT tunnel.
To ignore the proxy environment variables for a request or client, set the
proxy option to '': an explicit option decision is final, so this forces a
direct connection with no environment fallback. To send only selected hosts
directly, add them to the proxy option's no list, or list them in
no_proxy/NO_PROXY (* bypasses every host). These work identically on both
built-in handlers.
Handler-specific overrides remain available for finer transport control when
they do not conflict with Guzzle-managed behavior. The built-in cURL handlers
now reject raw cURL options that override request method, URI, body, headers,
timeouts, redirects, proxy URLs and types, TLS verification or client
credentials, progress/debug callbacks, sink handling, cookies, protocols,
connection coalescing, or cURL share handles. Use first-class Guzzle request
options for those settings. Allowed raw cURL header-list options, such as
CURLOPT_PROXYHEADER, now accept only strings or stringable objects as entries.
The cURL handlers also reject stream-only stream_context options, but accept
read_timeout without effect. The stream handler rejects cURL-only options it
cannot honor, but accepts connect_timeout without effect. These timeout options
are intentionally best-effort so shared request configuration can be reused
across transports.
Automatic Expect: 100-Continue injection now applies only to HTTP/1.1 requests.
HTTP/1.0, HTTP/2, and HTTP/3 requests do not receive the header from Guzzle's
body-preparation middleware.
The stream handler rejects an explicit expect option when it results in an
Expect header, because PHP streams do not support the 100-Continue workflow.
Set expect => false for stream-handler requests that must avoid this rejection.
Guzzle 8 adds native parameter and return types where PHP 7.4 allows. Code overriding affected methods must update method signatures accordingly.
SetCookie::__toString() now returns string.
HandlerStack::remove() now throws TypeError when passed a value that is
neither a callable nor a middleware name string.
Pool::__construct() and Pool::batch() now require iterable request collections.
SetCookie::setSecure(), SetCookie::setDiscard(), and
SetCookie::setHttpOnly() now require boolean parameters. Calls from files that
declare strict types will throw TypeError for non-boolean values.
SetCookie::getExpires() now returns int|null. Invalid textual expiration
dates are treated as null.
The idn_conversion request option must be true, false, null, or an
integer IDNA_* bitmask. Numeric strings and floats that previously worked
through PHP scalar coercion are no longer accepted.
Integer 0 remains a valid option bitmask. Use false or null to disable IDN
conversion.
Guzzle's async client APIs, handlers, and middleware callable annotations now use
generic PromiseInterface<ResponseInterface, mixed> PHPDoc types. This is a
static-analysis-only change at runtime, but projects with stricter static
analysis may see new or different diagnostics.
Code using unparameterized promise types continues to work. If your project implements Guzzle client interfaces, provides custom handlers or middleware, or uses stricter static analysis, you may need to update your PHPDoc annotations to include promise fulfillment and rejection types.
Public client config, request option, pool option, handler, middleware, mock
handler, and history middleware PHPDoc now uses structured array and callable
shapes. This does not change runtime behavior, but stricter static analysis may
now report invalid option keys, invalid option value types, or lower-arity
callback annotations that were previously hidden behind loose array or
callable PHPDoc.
If your project implements ClientInterface, extends client behavior through
traits, builds custom handlers or middleware, or documents reusable request
option arrays, update those PHPDoc annotations to match the supported request
option and callback shapes.
Guzzle 8 uses Guzzle PSR-7 3.x for multipart request bodies. Multipart parts
created through the multipart request option no longer include generated
per-part Content-Length headers by default. If your tests compare raw
multipart payloads, remove those generated part headers from expected strings.
Generated multipart Content-Disposition header name and filename
parameters now escape double quotes, carriage returns, and line feeds as %22,
%0D, and %0A. Literal backslashes and other characters are serialized
unchanged, matching browser multipart form submission behavior. Custom multipart
part header names and values, and explicit PSR-7 multipart boundaries, are also
validated by PSR-7.
Custom multipart part header values also preserve trailing spaces and tabs in the serialized request body, so raw body snapshots or signatures may need updated expectations.
Guzzle now quotes the boundary parameter in generated
Content-Type: multipart/form-data headers when an explicit PSR-7
MultipartStream boundary contains characters that require quoting.
Automatically generated boundaries are unchanged.
You can still pass an explicit Content-Length header in a multipart element's
headers array if a non-standard peer requires it.
With the optional referer redirect setting enabled (off by default), Guzzle
now sends only the request origin (scheme, host, and port) in the Referer
header on a cross-origin redirect. It previously sent the referring URI,
including the path, query string, and fragment, which could leak secrets such
as reset tokens or signed query parameters to the new origin.
Same-origin redirects still send the path and query, with any user information
and fragment removed. The Referer header is omitted entirely when the scheme
changes, including an https to http downgrade. This matches the
strict-origin-when-cross-origin policy that modern browsers use by default.
If you relied on the full URL crossing origins, collect it with the
on_redirect setting, or disable automatic redirects and follow them manually.
Guzzle now follows only the redirect status codes 301, 302, 303, 307, and 308
when allow_redirects is enabled. Other 3xx responses, including 300, 304, 305,
and 306, are returned to the caller unchanged even when they carry a Location
header, in line with RFC 9110 section 15.4. Code that relied on Guzzle following
one of those responses should handle it directly or inspect it with an
on_redirect callback.
Guzzle 8 ignores Secure cookies received over non-HTTPS requests. It also
ignores an insecure cookie received over a non-HTTPS request when its name and
domain overlap an existing Secure cookie and its path falls within the
existing cookie's protected path. This includes deletions sent with
Max-Age=0.
Guzzle 7 accepted these cookies. Applications using plain HTTP development
servers that send Secure cookies must use HTTPS or remove the attribute.
Cookies inserted directly through CookieJar::setCookie() or restored from
persistence are not rejected because they have no response origin. Existing
Secure records still protect against later cookies received over HTTP.
CookieJar::extractCookies() now recognizes the __Secure- and __Host-
prefixes case-insensitively. __Secure- response cookies must be Secure.
__Host- response cookies must also be host-only, include a Path attribute,
and use the root path. A bare Path without = remains ignored. Invalid
prefixed response cookies are ignored.
Cookie names remain case-sensitive, so differently cased names remain distinct.
Correct the attributes or rename a legacy response cookie that used a reserved
prefix without satisfying its requirements. Cookies inserted directly through
CookieJar::setCookie() and restored persisted records are unchanged.
Cookies extracted from responses without a Domain attribute are now stored as
host-only cookies. They are sent only to the exact host that set them.
Previously, Guzzle stored these cookies with the request host as a normal domain
cookie, so they could also be sent to subdomains. Applications relying on that
behavior should use an explicit Domain attribute.
SetCookie::toArray() may include HostOnly => true for host-only cookies.
Cookie domains with multiple leading dots, such as Domain=..example.com, are
no longer normalized twice during matching. Such malformed domains are rejected
instead of matching example.com or its subdomains.
Cookies with Max-Age=0 or a negative Max-Age are now treated as immediately
expired. When such a cookie is added to a CookieJar, it removes a matching
stored cookie instead of being retained as a normal cookie.
SetCookie constructor arrays no longer coerce invalid field values. Cookie
names, values, domains, paths, max-age values, expiry values, and boolean flags
must use the documented types. Invalid constructor values now throw
InvalidArgumentException.
// Valid:
new SetCookie([
'Name' => 'foo',
'Value' => 'bar',
'Domain' => 'example.com',
'Secure' => true,
]);
// Invalid in 8.0:
new SetCookie([
'Name' => false,
'Value' => 'bar',
]);
Cookies parsed from normal Set-Cookie headers continue to be normalized by
SetCookie::fromString(). Attributes that require a value (Domain, Path,
Expires, Max-Age) are ignored when they appear without one.
SetCookie now follows RFC cookie precedence when both Max-Age and Expires
are present. A valid Max-Age value controls the effective expiration time,
including Max-Age=0 expiring the cookie immediately, even when Expires is a
future date.
SetCookie::fromString() now ignores float-like or exponent Max-Age values
such as 0.5, 1.5, or 1e3. Guzzle 7 truncated these numeric forms toward
zero. Use integer-second Max-Age values.
SetCookie::fromString() now trims cookie names, cookie values, and attribute
segments with the RFC 6265 whitespace characters, space and horizontal tab.
Guzzle 7 also trimmed line feeds, carriage returns, null bytes, and vertical
tabs. Header values produced by Guzzle PSR-7 cannot contain those bytes, so
this only affects strings passed to SetCookie::fromString() directly.
CookieJar::clear() now treats only null as an omitted path or name.
Previously, falsy path or name values such as '0' or '' could be interpreted
as omitted and clear a broader set of cookies than intended.
If you call clear() to clear all cookies, continue passing no arguments:
$jar->clear();
If you pass a path or name, that value is now treated as provided.
CookieJar::getCookieByName() now matches cookie names case-sensitively. If a
jar contains distinct cookies such as SID and sid, retrieve each one with the
exact stored name.
FileCookieJar and SessionCookieJar can no longer be restored with
unserialize(); attempting to unserialize either jar now throws a
LogicException. Rebuild persisted jars instead: new FileCookieJar($path)
reloads the persisted cookie file when it exists, and
new SessionCookieJar($key) reloads the cookie data stored in the session.
FileCookieJar now writes its cookie file with owner-only permissions (0600),
so persisted cookies are not world-readable under the default umask; if another
user or process must read the file, adjust its permissions after saving. Saved
cookie files also JSON-escape tag characters without changing cookie values.
Persisted cookie data must now be a JSON list. Each list entry must decode to an
array, and recognized SetCookie fields must use their documented constructor
types. Malformed JSON, an invalid stored shape, or a recognized field with the
wrong type causes a RuntimeException. Cookie records are constructed before
any are passed to setCookie(), so such failures leave the jar unchanged.
Numeric or string-keyed JSON objects must be converted to lists.
Every cookie record must include an explicit boolean HostOnly marker. The
built-in jars write this marker automatically. Delete or rotate older nonempty
data without it, or annotate each record only when its original Domain
semantics are known.
In Guzzle 7, malformed JSON in a cookie file and FileCookieJar encoding
failures threw GuzzleHttp\Exception\InvalidArgumentException. Now they throw
RuntimeException, consistently with other persistent cookie failures. Both
jars expose the native JsonException through getPrevious() when JSON
encoding or decoding fails.
An empty cookie file remains a no-op. A missing or null session value still
means no stored cookie data. Any other session value must be a string containing
a JSON list; malformed JSON and an empty string are rejected. SessionCookieJar
does not replace the stored value when construction fails.
GuzzleHttp\MessageFormatter is now final. Applications that extended
MessageFormatter should implement GuzzleHttp\MessageFormatterInterface
instead and pass the custom formatter to GuzzleHttp\Middleware::log().
Middleware::log() now requires its formatter argument to implement
MessageFormatterInterface. Passing new MessageFormatter() still works
because MessageFormatter implements MessageFormatterInterface. Passing any
other value now fails with PHP's native TypeError instead of Guzzle's previous
LogicException.
use GuzzleHttp\MessageFormatterInterface;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
final class RedactingFormatter implements MessageFormatterInterface
{
public function format(
RequestInterface $request,
?ResponseInterface $response = null,
?\Throwable $error = null
): string {
return $request->getMethod().' '.$request->getUri()->getPath();
}
}
$stack->push(Middleware::log($logger, new RedactingFormatter()));
GuzzleHttp\Handler\CurlFactory, GuzzleHttp\Handler\CurlHandler,
GuzzleHttp\Handler\CurlMultiHandler, GuzzleHttp\Handler\MockHandler, and
GuzzleHttp\Handler\StreamHandler are now final.
Applications that extended CurlFactory should implement
GuzzleHttp\Handler\CurlFactoryInterface instead. Applications that extended
CurlHandler, CurlMultiHandler, MockHandler, or StreamHandler should use
composition instead: wrap a handler instance in a custom callable or provide a
custom handler rather than subclassing the built-in handler.
Custom GuzzleHttp\Handler\CurlFactoryInterface implementations that create or
mutate GuzzleHttp\Handler\EasyHandle instances must assign values compatible
with EasyHandle's documented public property types. Several EasyHandle
bookkeeping properties now use native property types, so assigning incompatible
values to those properties raises TypeError.
Custom factories must assign the request and sink state before returning an
EasyHandle to Guzzle because those properties are required typed invariants.
Reading them before assignment raises PHP's uninitialized typed-property Error.
The native cURL handle properties intentionally remain untyped because PHP 7.4
represents cURL handles as resources while PHP 8 represents them as cURL handle
objects. Custom factories must still unset $easy->handle when releasing an
easy handle, as required by CurlFactoryInterface::release().
CurlHandler, CurlMultiHandler, and StreamHandler now reject unknown
constructor option keys with GuzzleHttp\Exception\InvalidArgumentException.
Remove misspelled or application-specific keys before constructing built-in
handlers.
CurlMultiHandler now rejects cURL multi options that cannot be applied by the
installed runtime libcurl. Values passed through the constructor options key
must be an array keyed by integer CURLMOPT_* constants. Guzzle 7 already
fails closed when a named connection cap cannot be applied; Guzzle 8 extends
this rejection to every cURL multi option, including raw CURLMOPT_* entries
that Guzzle 7 only warns about.
CurlMultiHandler now rejects CURLMOPT_PIPELINING in the constructor
options array, deprecated since Guzzle 7.15. Pass Multiplexing::NONE as the
multiplex client option or, when constructing the handler yourself, as the
multiplex constructor option to disallow multiplexing on the handler
(replacing CURLPIPE_NOTHING and 0), or remove the option entirely for
multiplex-capable behavior (replacing masks containing CURLPIPE_MULTIPLEX);
multiplexing is on by default from libcurl 7.62, except on 7.65.0 and 7.65.1
where a regression dropped the default, and Guzzle 8's HTTP/2 floor of libcurl
7.65.2 is the version that restored it, so removing the option preserves
multiplex-capable behavior on every supported HTTP/2 runtime. CURLPIPE_HTTP1
and 1 also map to Multiplexing::NONE on libcurl 7.62 and newer, where
HTTP/1.1 pipelining is a no-op; on older libcurl they enabled HTTP/1.1
pipelining, which has no replacement. A handler configured with
Multiplexing::NONE wins over the default WAIT request mode: default requests
run without waiting, explicitly requested wait modes are rejected as a
configuration conflict when the transfer would actually wait, and the required
modes are always rejected. Multiplexing::NONE is also accepted as a request
option value exactly where its guarantee - the transfer does not share its
connection with any concurrent transfer - holds and can be verified; see the
multiplex request option documentation for the acceptance rules.
CurlMultiHandler now rejects CURLMOPT_MAX_HOST_CONNECTIONS and
CURLMOPT_MAX_TOTAL_CONNECTIONS entries in the constructor options array. Use
the max_host_connections and max_total_connections client options when
Guzzle creates the default handler, or the same named CurlMultiHandler
constructor options when constructing the handler directly.
Direct magic access to CurlMultiHandler::$_mh has been removed. This was an
undocumented internal lazy cURL multi handle. Applications that used it to set
CURLMOPT_* options should pass those values through the options key of the
CurlMultiHandler constructor.
The built-in handlers now pass integer byte counts to progress callbacks.
Callbacks with int parameter types continue to work, and callbacks with
float parameter types can still receive integer byte counts in PHP. If a
callback used other scalar parameter types, update it to accept integers or
remove the scalar parameter declarations.
The GUZZLE_CURL_SELECT_TIMEOUT environment variable is no longer read. Pass
the select_timeout option to CurlMultiHandler instead. The
select_timeout option must be numeric, finite, and non-negative. It must be
0 or greater than or equal to 0.001 seconds.
Client::__call() has been removed. The typed HTTP verb methods (get(),
head(), put(), post(), patch(), delete(), and their *Async()
variants) have been real methods since Guzzle 7.0 and are unaffected. Only
verbs without a typed method lose their magic form: calls such as
$client->options($uri), $client->trace($uri),
$client->optionsAsync($uri), or any custom verb such as
$client->purge($uri) now fail with a PHP undefined method error. Use
request() or requestAsync() with an explicit method instead:
// 7.x
$response = $client->options('http://example.com');
// 8.0
$response = $client->request('OPTIONS', 'http://example.com');
ClientInterface::getConfig() has been removed from the interface. The
concrete Client::getConfig() method remains available and is no longer
deprecated. Code reading configuration through a ClientInterface-typed
value must type against Client instead, and custom ClientInterface
implementations no longer need to provide getConfig(), although keeping the
method still satisfies the interface.
The deprecated GuzzleHttp namespace functions were removed, along with the
functions.php and functions_include.php files and the Composer files
autoload entry that loaded them on every request.
Replace namespaced function calls with their native or class equivalents:
// Before:
use function GuzzleHttp\json_decode;
$data = json_decode($json);
// After:
$data = \json_decode($json, false, 512, \JSON_THROW_ON_ERROR);
| Original Function | Replacement |
|---|---|
describe_type | PHP's get_debug_type |
headers_from_lines | Utils::headersFromLines |
debug_resource | Utils::debugResource |
choose_handler | Utils::chooseHandler |
default_user_agent | Utils::defaultUserAgent |
default_ca_bundle | none; use the system trust store or verify |
normalize_header_keys | Utils::normalizeHeaderKeys |
is_host_in_noproxy | ProxyOptions::isHostInNoProxy |
json_decode | PHP's json_decode with JSON_THROW_ON_ERROR |
json_encode | PHP's json_encode with JSON_THROW_ON_ERROR |
The deprecated Utils::jsonDecode() and Utils::jsonEncode() methods have
also been removed. Use PHP's native JSON functions with
JSON_THROW_ON_ERROR. Failures throw JsonException directly.
See the "Removed Proxy Helper API" section below for is_host_in_noproxy()
behavior differences. The deprecated Utils::defaultCaBundle() and the
internal Utils::isUriInNoProxy() helpers have also been removed; use the
verify option and ProxyOptions::isUriInNoProxy() respectively.
RetryMiddleware::exponentialDelay() has been removed. The retry middleware
continues to use the same exponential backoff calculation by default. This only
affects code that called the static helper directly; inline that calculation or
pass a custom delay callable to Middleware::retry().
RedirectMiddleware::$defaultSettings has been removed. Use
RedirectMiddleware::DEFAULT_SETTINGS instead.
Utils::isHostInNoProxy() has been removed.
Use ProxyOptions::resolve() when implementing Guzzle-compatible proxy handling
in a custom handler. Use ProxyOptions::isUriInNoProxy() when checking whether
a request URI matches a no-proxy list. Use ProxyOptions::isHostInNoProxy()
only when checking a host string directly. The environment-variable fallback
performed by the built-in handlers is not part of ProxyOptions::resolve();
custom handlers that want it must implement their own environment lookup.
These helpers share the normalized no-proxy matching of the Guzzle 7 Utils
helpers: domain matching is case-insensitive and ignores a single trailing DNS
root dot, IP literals are normalized before comparison, and CIDR entries match
IP literal hosts. They differ from the Guzzle 7 helpers in three ways — they
throw InvalidArgumentException for non-string list entries where Guzzle 7
skips them, a leading-dot entry such as .example.com also matches the bare
domain, and string no-proxy lists are split on whitespace as well as commas.
Utils::describeType() has been removed. Use PHP's get_debug_type() instead.
Static utility and constant classes such as GuzzleHttp\Middleware,
GuzzleHttp\Utils, GuzzleHttp\RequestOptions,
GuzzleHttp\Handler\HeaderProcessor, and GuzzleHttp\Handler\Proxy now have
private constructors. GuzzleHttp\Handler\Proxy is also declared final.
These classes only expose static members. Replace any accidental instantiation with static method calls or constant access.
HandlerStack::__toString() has been removed.
Runtime objects such as clients, handler stacks, middleware, handlers, pools,
mock handlers, cURL transport objects, and persistent cookie jars no longer
support native PHP serialize() or unserialize(). Persist configuration or
cookie data explicitly and rebuild runtime objects during bootstrap.
Guzzle 8 marks credential-bearing parameters with SensitiveParameter. On PHP
8.2 and later, selected exception-trace arguments are represented by
SensitiveParameterValue instead of exposing the original argument. PHP 7.4
through 8.1 do not redact trace arguments. The attribute does not redact logs,
exception messages, HTTP traffic, properties, captured variables, return
values, custom callback frames, or the separate $this/object entry in
explicit backtraces. PHP 8.2 also does not reliably redact named values
collected by an attributed variadic; PHP 8.3+ does.
In order to take advantage of the new features of PHP, Guzzle dropped the support of PHP 5. The minimum supported PHP version is now PHP 7.2. Type hints and return types for functions and methods have been added wherever possible.
Please make sure:
GuzzleHttp\UriTemplate is removed.GuzzleHttp\Exception\SeekException is removed.GuzzleHttp\Exception\BadResponseException, GuzzleHttp\Exception\ClientException,
GuzzleHttp\Exception\ServerException can no longer be initialized with an empty
Response as argument.GuzzleHttp\Exception\ConnectException now extends GuzzleHttp\Exception\TransferException
instead of GuzzleHttp\Exception\RequestException.GuzzleHttp\Exception\ConnectException::getResponse() is removed.GuzzleHttp\Exception\ConnectException::hasResponse() is removed.GuzzleHttp\ClientInterface::VERSION is removed. Added GuzzleHttp\ClientInterface::MAJOR_VERSION instead.GuzzleHttp\Exception\RequestException::getResponseBodySummary is removed.
Use \GuzzleHttp\Psr7\get_message_body_summary as an alternative.GuzzleHttp\Cookie\CookieJar::getCookieValue is removed.exceptions is removed. Please use http_errors.save_to is removed. Please use sink.pool_size is removed. Please use concurrency.$_SERVER super global, due to thread safety issues with getenv. We continue to fallback to getenv in CLI environments, for maximum compatibility.get, head, put, post, patch, delete, getAsync, headAsync, putAsync, postAsync, patchAsync, and deleteAsync methods are now implemented as genuine methods on GuzzleHttp\Client, with strong typing. The original __call implementation remains unchanged for now, for maximum backwards compatibility, but won't be invoked under normal operation.log middleware will log the errors with level error instead of noticeAll internal native functions calls of Guzzle are now prefixed with a slash. This change makes it impossible for method overloading by other libraries or applications. Example:
// Before:
curl_version();
// After:
\curl_version();
For the full diff you can check here.
Guzzle now uses PSR-7 for HTTP messages.
Due to the fact that these messages are immutable, this prompted a refactoring
of Guzzle to use a middleware based system rather than an event system. Any
HTTP message interaction (e.g., GuzzleHttp\Message\Request) need to be
updated to work with the new immutable PSR-7 request and response objects. Any
event listeners or subscribers need to be updated to become middleware
functions that wrap handlers (or are injected into a
GuzzleHttp\HandlerStack).
GuzzleHttp\BatchResultsGuzzleHttp\CollectionGuzzleHttp\HasDataTraitGuzzleHttp\ToArrayInterfaceguzzlehttp/streams dependency has been removed. Stream functionality
is now present in the GuzzleHttp\Psr7 namespace provided by the
guzzlehttp/psr7 package.guzzlehttp/promises library. We use a custom promise library for three
significant reasons:
GuzzleHttp\Mimetypes has been moved to a function in
GuzzleHttp\Psr7\mimetype_from_extension and
GuzzleHttp\Psr7\mimetype_from_filename.GuzzleHttp\Query and GuzzleHttp\QueryParser have been removed. Query
strings must now be passed into request objects as strings, or provided to
the query request option when creating requests with clients. The query
option uses PHP's http_build_query to convert an array to a string. If you
need a different serialization technique, you will need to pass the query
string in as a string. There are a couple helper functions that will make
working with query strings easier: GuzzleHttp\Psr7\parse_query and
GuzzleHttp\Psr7\build_query.GuzzleHttp\Handler namespace. This significantly reduces
complexity in Guzzle, removes a dependency, and improves performance. RingPHP
will be maintained for Guzzle 5 support, but will no longer be a part of
Guzzle 6.Event namespace.Subscriber namespace.Transaction classRequestFsmRingBridgeGuzzleHttp\Subscriber\Cookie is now provided by
GuzzleHttp\Middleware::cookiesGuzzleHttp\Subscriber\HttpError is now provided by
GuzzleHttp\Middleware::httpErrorGuzzleHttp\Subscriber\History is now provided by
GuzzleHttp\Middleware::historyGuzzleHttp\Subscriber\Mock is now provided by
GuzzleHttp\Handler\MockHandlerGuzzleHttp\Subscriber\Prepare is now provided by
GuzzleHttp\PrepareBodyMiddlewareGuzzleHttp\Subscriber\Redirect is now provided by
GuzzleHttp\RedirectMiddlewarePsr\Http\Message\UriInterface (implements in
GuzzleHttp\Psr7\Uri) for URI support. GuzzleHttp\Url is now gone.GuzzleHttp\Utils have been moved to namespaced
functions under the GuzzleHttp namespace. This requires either a Composer
based autoloader or you to include functions.php.GuzzleHttp\ClientInterface::getDefaultOption has been renamed to
GuzzleHttp\ClientInterface::getConfig.GuzzleHttp\ClientInterface::setDefaultOption has been removed.json and xml methods of response objects has been removed. With the
migration to strictly adhering to PSR-7 as the interface for Guzzle messages,
adding methods to message interfaces would actually require Guzzle messages
to extend from PSR-7 messages rather then work with them directly.The change to PSR-7 unfortunately required significant refactoring to Guzzle due to the fact that PSR-7 messages are immutable. Guzzle 5 relied on an event system from plugins. The event system relied on mutability of HTTP messages and side effects in order to work. With immutable messages, you have to change your workflow to become more about either returning a value (e.g., functional middlewares) or setting a value on an object. Guzzle v6 has chosen the functional middleware approach.
Instead of using the event system to listen for things like the before event,
you now create a stack based middleware function that intercepts a request on
the way in and the promise of the response on the way out. This is a much
simpler and more predictable approach than the event system and works nicely
with PSR-7 middleware. Due to the use of promises, the middleware system is
also asynchronous.
v5:
use GuzzleHttp\Event\BeforeEvent;
$client = new GuzzleHttp\Client();
// Get the emitter and listen to the before event.
$client->getEmitter()->on('before', function (BeforeEvent $e) {
// Guzzle v5 events relied on mutation
$e->getRequest()->setHeader('X-Foo', 'Bar');
});
v6:
In v6, you can modify the request before it is sent using the mapRequest
middleware. The idiomatic way in v6 to modify the request/response lifecycle is
to setup a handler middleware stack up front and inject the handler into a
client.
use GuzzleHttp\Middleware;
// Create a handler stack that has all of the default middlewares attached
$handler = GuzzleHttp\HandlerStack::create();
// Push the handler onto the handler stack
$handler->push(Middleware::mapRequest(function (RequestInterface $request) {
// Notice that we have to return a request object
return $request->withHeader('X-Foo', 'Bar');
}));
// Inject the handler into the client
$client = new GuzzleHttp\Client(['handler' => $handler]);
This version added the form_params
and multipart request options. form_params is an associative array of
strings or array of strings and is used to serialize an
application/x-www-form-urlencoded POST request. The
multipart
option is now used to send a multipart/form-data POST request.
GuzzleHttp\Post\PostFile has been removed. Use the multipart option to add
POST files to a multipart/form-data request.
The body option no longer accepts an array to send POST requests. Please use
multipart or form_params instead.
The base_url option has been renamed to base_uri.
Guzzle now uses RingPHP to send
HTTP requests. The adapter option in a GuzzleHttp\Client constructor
is still supported, but it has now been renamed to handler. Instead of
passing a GuzzleHttp\Adapter\AdapterInterface, you must now pass a PHP
callable that follows the RingPHP specification.
Fluent interfaces were removed from the following classes:
GuzzleHttp\CollectionGuzzleHttp\UrlGuzzleHttp\QueryGuzzleHttp\Post\PostBodyGuzzleHttp\Cookie\SetCookieRemoved "functions.php", so that Guzzle is truly PSR-4 compliant. The following functions can be used as replacements.
GuzzleHttp\json_decode -> PHP's json_decodeGuzzleHttp\get_path -> GuzzleHttp\Utils::getPathGuzzleHttp\Utils::setPath -> GuzzleHttp\set_pathGuzzleHttp\Pool::batch -> GuzzleHttp\batch. This function is, however,
deprecated in favor of using GuzzleHttp\Pool::batch().The "procedural" global client has been removed with no replacement (e.g.,
GuzzleHttp\get(), GuzzleHttp\post(), etc.). Use a GuzzleHttp\Client
object as a replacement.
throwImmediately has been removedThe concept of "throwImmediately" has been removed from exceptions and error events. This control mechanism was used to stop a transfer of concurrent requests from completing. This can now be handled by throwing the exception or by cancelling a pool of requests or each outstanding future request individually.
Removed the "headers" event. This event was only useful for changing the body a response once the headers of the response were known. You can implement a similar behavior in a number of ways. One example might be to use a FnStream that has access to the transaction being sent. For example, when the first byte is written, you could check if the response headers match your expectations, and if so, change the actual stream body that is being written to.
Removed the asArray parameter from
GuzzleHttp\Message\MessageInterface::getHeader. If you want to get a header
value as an array, then use the newly added getHeaderAsArray() method of
MessageInterface. This change makes the Guzzle interfaces compatible with
the PSR-7 interfaces.
GuzzleHttp\Event\EmitterInterface (resulting in significant
speed and functionality improvements).Changes per Guzzle 3.x namespace are described below.
The Guzzle\Batch namespace has been removed. This is best left to
third-parties to implement on top of Guzzle's core HTTP library.
The Guzzle\Cache namespace has been removed. (Todo: No suitable replacement
has been implemented yet, but hoping to utilize a PSR cache interface).
FromConfigInterface has been removed.Guzzle\Common\Version has been removed. The VERSION constant can be found
at GuzzleHttp\ClientInterface::VERSION.getAll has been removed. Use toArray to convert a collection to an array.inject has been removed.keySearch has been removed.getPath no longer supports wildcard expressions. Use something better like
JMESPath for this.setPath now supports appending to an existing array via the [] notation.Guzzle no longer requires Symfony's EventDispatcher component. Guzzle now uses
GuzzleHttp\Event\Emitter.
Symfony\Component\EventDispatcher\EventDispatcherInterface is replaced by
GuzzleHttp\Event\EmitterInterface.Symfony\Component\EventDispatcher\EventDispatcher is replaced by
GuzzleHttp\Event\Emitter.Symfony\Component\EventDispatcher\Event is replaced by
GuzzleHttp\Event\Event, and Guzzle now has an EventInterface in
GuzzleHttp\Event\EventInterface.AbstractHasDispatcher has moved to a trait, HasEmitterTrait, and
HasDispatcherInterface has moved to HasEmitterInterface. Retrieving the
event emitter of a request, client, etc. now uses the getEmitter method
rather than the getDispatcher method.once() method to add a listener that automatically removes itself
the first time it is invoked.listeners() method to retrieve a list of event listeners rather than
the getListeners() method.emit() instead of dispatch() to emit an event from an emitter.attach() instead of addSubscriber() and detach() instead of
removeSubscriber().$mock = new Mock();
// 3.x
$request->getEventDispatcher()->addSubscriber($mock);
$request->getEventDispatcher()->removeSubscriber($mock);
// 4.x
$request->getEmitter()->attach($mock);
$request->getEmitter()->detach($mock);
Use the on() method to add a listener rather than the addListener() method.
// 3.x
$request->getEventDispatcher()->addListener('foo', function (Event $event) { /* ... */ } );
// 4.x
$request->getEmitter()->on('foo', function (Event $event, $name) { /* ... */ } );
src/cacert.pem.complete and error
events to asynchronously manage parallel request transfers.Guzzle\Http\Url has moved to GuzzleHttp\Url.Guzzle\Http\QueryString has moved to GuzzleHttp\Query.GuzzleHttp\StaticClient has been removed. Use the functions provided in
functions.php for an easy to use static client instance.GuzzleHttp\Exception have been updated to all extend from
GuzzleHttp\Exception\TransferException.Calling methods like get(), post(), head(), etc. no longer create and
return a request, but rather creates a request, sends the request, and returns
the response.
// 3.0
$request = $client->get('/');
$response = $request->send();
// 4.0
$response = $client->get('/');
// or, to mirror the previous behavior
$request = $client->createRequest('GET', '/');
$response = $client->send($request);
GuzzleHttp\ClientInterface has changed.
send method no longer accepts more than one request. Use sendAll to
send multiple requests in parallel.setUserAgent() has been removed. Use a default request option instead. You
could, for example, do something like:
$client->setConfig('defaults/headers/User-Agent', 'Foo/Bar ' . $client::getDefaultUserAgent()).setSslVerification() has been removed. Use default request options instead,
like $client->setConfig('defaults/verify', true).GuzzleHttp\Client has changed.
base_url string or array to use a URI template as the base URL of a client.
You can also specify a defaults key that is an associative array of default
request options. You can pass an adapter to use a custom adapter,
batch_adapter to use a custom adapter for sending requests in parallel, or
a message_factory to change the factory used to create HTTP requests and
responses.client.create_request event.createRequest, get, put, etc.) in order to expand a URI template.Messages no longer have references to their counterparts (i.e., a request no
longer has a reference to it's response, and a response no loger has a
reference to its request). This association is now managed through a
GuzzleHttp\Adapter\TransactionInterface object. You can get references to
these transaction objects using request events that are emitted over the
lifecycle of a request.
GuzzleHttp\Message\EntityEnclosingRequest and
GuzzleHttp\Message\EntityEnclosingRequestInterface have been removed. The
separation between requests that contain a body and requests that do not
contain a body has been removed, and now GuzzleHttp\Message\RequestInterface
handles both use cases.GuzzleHttp\Response object now accept a
GuzzleHttp\Message\ResponseInterface.GuzzleHttp\Message\RequestFactoryInterface has been renamed to
GuzzleHttp\Message\MessageFactoryInterface. This interface is used to create
both requests and responses and is implemented in
GuzzleHttp\Message\MessageFactory.GuzzleHttp\Post\PostBodyInterface
to control the format of a POST body. Requests that are created using a
standard GuzzleHttp\Message\MessageFactoryInterface will automatically use
a GuzzleHttp\Post\PostBody body if the body was passed as an array or if
the method is POST and no body is provided.$request = $client->createRequest('POST', '/');
$request->getBody()->setField('foo', 'bar');
$request->getBody()->addFile(new PostFile('file_key', fopen('/path/to/content', 'r')));
GuzzleHttp\Message\Header has been removed. Header values are now simply
represented by an array of values or as a string. Header values are returned
as a string by default when retrieving a header value from a message. You can
pass an optional argument of true to retrieve a header value as an array
of strings instead of a single concatenated string.GuzzleHttp\PostFile and GuzzleHttp\PostFileInterface have been moved to
GuzzleHttp\Post. This interface has been simplified and now allows the
addition of arbitrary headers.GuzzleHttp\Message\Header\Link have been removed. Most
of the custom headers are now handled separately in specific
subscribers/plugins, and GuzzleHttp\Message\HeaderValues::parseParams() has
been updated to properly handle headers that contain parameters (like the
Link header).GuzzleHttp\Message\Response::getInfo() and
GuzzleHttp\Message\Response::setInfo() have been removed. Use the event
system to retrieve this type of information.GuzzleHttp\Message\Response::getRawHeaders() has been removed.GuzzleHttp\Message\Response::getMessage() has been removed.GuzzleHttp\Message\Response::calculateAge() and other cache specific
methods have moved to the CacheSubscriber.getContentMd5() have been removed.
Just use getHeader('Content-MD5') instead.GuzzleHttp\Message\Response::setRequest() and
GuzzleHttp\Message\Response::getRequest() have been removed. Use the event
system to work with request and response objects as a transaction.GuzzleHttp\Message\Response::getRedirectCount() has been removed. Use the
Redirect subscriber instead.GuzzleHttp\Message\Response::isSuccessful() and other related methods have
been removed. Use getStatusCode() instead.Streaming requests can now be created by a client directly, returning a
GuzzleHttp\Message\ResponseInterface object that contains a body stream
referencing an open PHP HTTP stream.
// 3.0
use Guzzle\Stream\PhpStreamRequestFactory;
$request = $client->get('/');
$factory = new PhpStreamRequestFactory();
$stream = $factory->fromRequest($request);
$data = $stream->read(1024);
// 4.0
$response = $client->get('/', ['stream' => true]);
// Read some data off of the stream in the response body
$data = $response->getBody()->read(1024);
The configureRedirects() method has been removed in favor of a
allow_redirects request option.
// Standard redirects with a default of a max of 5 redirects
$request = $client->createRequest('GET', '/', ['allow_redirects' => true]);
// Strict redirects with a custom number of redirects
$request = $client->createRequest('GET', '/', [
'allow_redirects' => ['max' => 5, 'strict' => true]
]);
EntityBody interfaces and classes have been removed or moved to
GuzzleHttp\Stream. All classes and interfaces that once required
GuzzleHttp\EntityBodyInterface now require
GuzzleHttp\Stream\StreamInterface. Creating a new body for a request no
longer uses GuzzleHttp\EntityBody::factory but now uses
GuzzleHttp\Stream\Stream::factory or even better:
GuzzleHttp\Stream\create().
Guzzle\Http\EntityBodyInterface is now GuzzleHttp\Stream\StreamInterfaceGuzzle\Http\EntityBody is now GuzzleHttp\Stream\StreamGuzzle\Http\CachingEntityBody is now GuzzleHttp\Stream\CachingStreamGuzzle\Http\ReadLimitEntityBody is now GuzzleHttp\Stream\LimitStreamGuzzle\Http\IoEmittyinEntityBody has been removed.Requests previously submitted a large number of requests. The number of events
emitted over the lifecycle of a request has been significantly reduced to make
it easier to understand how to extend the behavior of a request. All events
emitted during the lifecycle of a request now emit a custom
GuzzleHttp\Event\EventInterface object that contains context providing
methods and a way in which to modify the transaction at that specific point in
time (e.g., intercept the request and set a response on the transaction).
request.before_send has been renamed to before and now emits a
GuzzleHttp\Event\BeforeEventrequest.complete has been renamed to complete and now emits a
GuzzleHttp\Event\CompleteEvent.request.sent has been removed. Use complete.request.success has been removed. Use complete.error is now an event that emits a GuzzleHttp\Event\ErrorEvent.request.exception has been removed. Use error.request.receive.status_line has been removed.curl.callback.progress has been removed. Use a custom StreamInterface to
maintain a status update.curl.callback.write has been removed. Use a custom StreamInterface to
intercept writes.curl.callback.read has been removed. Use a custom StreamInterface to
intercept reads.headers is a new event that is emitted after the response headers of a
request have been received before the body of the response is downloaded. This
event emits a GuzzleHttp\Event\HeadersEvent.
You can intercept a request and inject a response using the intercept() event
of a GuzzleHttp\Event\BeforeEvent, GuzzleHttp\Event\CompleteEvent, and
GuzzleHttp\Event\ErrorEvent event.
The Guzzle\Inflection namespace has been removed. This is not a core concern
of Guzzle.
The Guzzle\Iterator namespace has been removed.
Guzzle\Iterator\AppendIterator, Guzzle\Iterator\ChunkedIterator, and
Guzzle\Iterator\MethodProxyIterator are nice, but not a core requirement of
Guzzle itself.Guzzle\Iterator\FilterIterator is no longer needed because an equivalent
class is shipped with PHP 5.4.Guzzle\Iterator\MapIterator is not really needed when using PHP 5.5 because
it's easier to just wrap an iterator in a generator that maps values.For a replacement of these iterators, see https://github.com/nikic/iter
The LogPlugin has moved to https://github.com/guzzle/log-subscriber. The
Guzzle\Log namespace has been removed. Guzzle now relies on
Psr\Log\LoggerInterface for all logging. The MessageFormatter class has been
moved to GuzzleHttp\Subscriber\Log\Formatter.
The Guzzle\Parser namespace has been removed. This was previously used to
make it possible to plug in custom parsers for cookies, messages, URI
templates, and URLs; however, this level of complexity is not needed in Guzzle
so it has been removed.
GuzzleHttp\Cookie\SetCookie::fromString.GuzzleHttp\Message\MessageFactory::fromMessage. Message parsing is only
used in debugging or deserializing messages, so it doesn't make sense for
Guzzle as a library to add this level of complexity to parsing messages.GuzzleHttp\UriTemplate. The Guzzle library will automatically use the PECL
URI template library if it is installed.GuzzleHttp\Url::fromString (previously
it was Guzzle\Http\Url::factory()). If custom URL parsing is necessary,
then developers are free to subclass GuzzleHttp\Url.The Guzzle\Plugin namespace has been renamed to GuzzleHttp\Subscriber.
Several plugins are shipping with the core Guzzle library under this namespace.
GuzzleHttp\Subscriber\Cookie: Replaces the old CookiePlugin. Cookie jar
code has moved to GuzzleHttp\Cookie.GuzzleHttp\Subscriber\History: Replaces the old HistoryPlugin.GuzzleHttp\Subscriber\HttpError: Throws errors when a bad HTTP response is
received.GuzzleHttp\Subscriber\Mock: Replaces the old MockPlugin.GuzzleHttp\Subscriber\Prepare: Prepares the body of a request just before
sending. This subscriber is attached to all requests by default.GuzzleHttp\Subscriber\Redirect: Replaces the RedirectPlugin.The following plugins have been removed (third-parties are free to re-implement these if needed):
GuzzleHttp\Plugin\Async has been removed.GuzzleHttp\Plugin\CurlAuth has been removed.GuzzleHttp\Plugin\ErrorResponse\ErrorResponsePlugin has been removed. This
functionality should instead be implemented with event listeners that occur
after normal response parsing occurs in the guzzle/command package.The following plugins are not part of the core Guzzle package, but are provided in separate repositories:
Guzzle\Http\Plugin\BackoffPlugin has been rewritten to be much simpler
to build custom retry policies using simple functions rather than various
chained classes. See: https://github.com/guzzle/retry-subscriberGuzzle\Http\Plugin\Cache\CachePlugin has moved to
https://github.com/guzzle/cache-subscriberGuzzle\Http\Plugin\Log\LogPlugin has moved to
https://github.com/guzzle/log-subscriberGuzzle\Http\Plugin\Md5\Md5Plugin has moved to
https://github.com/guzzle/message-integrity-subscriberGuzzle\Http\Plugin\Mock\MockPlugin has moved to
GuzzleHttp\Subscriber\MockSubscriber.Guzzle\Http\Plugin\Oauth\OauthPlugin has moved to
https://github.com/guzzle/oauth-subscriberThe service description layer of Guzzle has moved into two separate packages:
Stream have moved to a separate package available at https://github.com/guzzle/streams.
Guzzle\Stream\StreamInterface has been given a large update to cleanly take
on the responsibilities of Guzzle\Http\EntityBody and
Guzzle\Http\EntityBodyInterface now that they have been removed. The number
of methods implemented by the StreamInterface has been drastically reduced to
allow developers to more easily extend and decorate stream behavior.
getStream and setStream have been removed to better encapsulate streams.getMetadata and setMetadata have been removed in favor of
GuzzleHttp\Stream\MetadataStreamInterface.getWrapper, getWrapperData, getStreamType, and getUri have all been
removed. This data is accessible when
using streams that implement GuzzleHttp\Stream\MetadataStreamInterface.rewind has been removed. Use seek(0) for a similar behavior.detachStream has been renamed to detach.feof has been renamed to eof.ftell has been renamed to tell.readLine has moved from an instance method to a static class method of
GuzzleHttp\Stream\Stream.GuzzleHttp\Stream\MetadataStreamInterface has been added to denote streams
that contain additional metadata accessible via getMetadata().
GuzzleHttp\Stream\StreamInterface::getMetadata and
GuzzleHttp\Stream\StreamInterface::setMetadata have been removed.
The entire concept of the StreamRequestFactory has been removed. The way this
was used in Guzzle 3 broke the actual interface of sending streaming requests
(instead of getting back a Response, you got a StreamInterface). Streaming
PHP requests are now implemented through the GuzzleHttp\Adapter\StreamAdapter.
\Guzzle\Common\Version::$emitWarnings = true;
The following APIs and options have been marked as deprecated:
Guzzle\Http\Message\Request::isResponseBodyRepeatable() as deprecated. Use $request->getResponseBody()->isRepeatable() instead.Guzzle\Http\Message\Request::canCache() as deprecated. Use Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest() instead.Guzzle\Http\Message\Request::canCache() as deprecated. Use Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest() instead.Guzzle\Http\Message\Request::setIsRedirect() as deprecated. Use the HistoryPlugin instead.Guzzle\Http\Message\Request::isRedirect() as deprecated. Use the HistoryPlugin instead.Guzzle\Cache\CacheAdapterFactory::factory() as deprecatedGuzzle\Service\Client::enableMagicMethods() as deprecated. Magic methods can no longer be disabled on a Guzzle\Service\Client.Guzzle\Parser\Url\UrlParser as deprecated. Just use PHP's parse_url() and percent encode your UTF-8.Guzzle\Common\Collection::inject() as deprecated.Guzzle\Plugin\CurlAuth\CurlAuthPlugin as deprecated. Use
$client->getConfig()->setPath('request.options/auth', array('user', 'pass', 'Basic|Digest|NTLM|Any')); or
$client->setDefaultOption('auth', array('user', 'pass', 'Basic|Digest|NTLM|Any'));3.7 introduces request.options as a parameter for a client configuration and as an optional argument to all creational
request methods. When paired with a client's configuration settings, these options allow you to specify default settings
for various aspects of a request. Because these options make other previous configuration options redundant, several
configuration options and methods of a client and AbstractCommand have been deprecated.
Marked Guzzle\Service\Client::getDefaultHeaders() as deprecated. Use $client->getDefaultOption('headers').
Marked Guzzle\Service\Client::setDefaultHeaders() as deprecated. Use $client->setDefaultOption('headers/{header_name}', 'value').
Marked 'request.params' for Guzzle\Http\Client as deprecated. Use $client->setDefaultOption('params/{param_name}', 'value')
Marked 'command.headers', 'command.response_body' and 'command.on_complete' as deprecated for AbstractCommand. These will work through Guzzle 4.0
$command = $client->getCommand('foo', array(
'command.headers' => array('Test' => '123'),
'command.response_body' => '/path/to/file'
));
// Should be changed to:
$command = $client->getCommand('foo', array(
'command.request_options' => array(
'headers' => array('Test' => '123'),
'save_as' => '/path/to/file'
)
));
Additions and changes (you will need to update any implementations or subclasses you may have created):
$options argument to the end of the following methods of Guzzle\Http\ClientInterface:
createRequest, head, delete, put, patch, post, options, prepareRequest$options argument to the end of Guzzle\Http\Message\Request\RequestFactoryInterface::createRequest()applyOptions() method to Guzzle\Http\Message\Request\RequestFactoryInterfaceGuzzle\Http\ClientInterface::get($uri = null, $headers = null, $body = null) to
Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $options = array()). You can still pass in a
resource, string, or EntityBody into the $options parameter to specify the download location of the response.Guzzle\Common\Collection::__construct($data) to no longer accepts a null value for $data but a
default array()Guzzle\Stream\StreamInterface::isRepeatableGuzzle\Http\Client::expandTemplate and getUriTemplate protected methods.The following methods were removed from interfaces. All of these methods are still available in the concrete classes that implement them, but you should update your code to use alternative methods:
Guzzle\Http\ClientInterface::setDefaultHeaders(). Use $client->getConfig()->setPath('request.options/headers/{header_name}', 'value'). or $client->getConfig()->setPath('request.options/headers', array('header_name' => 'value'))or$client->setDefaultOption('headers/{header_name}', 'value'). or $client->setDefaultOption('headers', array('header_name' => 'value'))`.Guzzle\Http\ClientInterface::getDefaultHeaders(). Use $client->getConfig()->getPath('request.options/headers')`.Guzzle\Http\ClientInterface::expandTemplate(). This is an implementation detail.Guzzle\Http\ClientInterface::setRequestFactory(). This is an implementation detail.Guzzle\Http\ClientInterface::getCurlMulti(). This is a very specific implementation detail.Guzzle\Http\Message\RequestInterface::canCache. Use the CachePlugin.Guzzle\Http\Message\RequestInterface::setIsRedirect. Use the HistoryPlugin.Guzzle\Http\Message\RequestInterface::isRedirect. Use the HistoryPlugin.CacheStorageInterface::cache($key, Response $response, $ttl = null) has changed to cache(RequestInterface $request, Response $response);CacheStorageInterface::fetch($key) has changed to fetch(RequestInterface $request);CacheStorageInterface::delete($key) has changed to delete(RequestInterface $request);CacheStorageInterface::purge($url)DefaultRevalidation::__construct(CacheKeyProviderInterface $cacheKey, CacheStorageInterface $cache, CachePlugin $plugin) has changed to DefaultRevalidation::__construct(CacheStorageInterface $cache, CanCacheStrategyInterface $canCache = null)RevalidationInterface::shouldRevalidate(RequestInterface $request, Response $response)If you previously relied on Guzzle\Http\Message\Header::raw(), then you will need to update your code to use the HeaderInterface (e.g. toArray(), getAll(), etc.).
Guzzle\Service\Command\CommandInterface typehint now request a
Guzzle\Service\Command\ArrayCommandInterface.Guzzle\Http\Message\RequestInterface::startResponse() to the RequestInterface to handle injecting a response
on a request while the request is still being transferredGuzzle\Service\Command\CommandInterface now extends from ToArrayInterface and ArrayAccessBase URLs of a client now follow the rules of https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.2 when merging URLs.
Guzzle\Http\Message\Response::getEtag() no longer strips quotes around the ETag response header
Guzzle\Http\UtilsThe Guzzle\Http\Utils class was removed. This class was only used for testing.
Guzzle\Stream\Stream::getWrapper() and Guzzle\Stream\Stream::getStreamType() are no longer converted to lowercase.
Emitting IO events from a RequestMediator is now a parameter that must be set in a request's curl options using the 'emit_io' key. This was previously set under a request's parameters using 'curl.emit_io'
Before 3.2, the same CurlMulti object was reused globally for each client. This can cause issue where plugins added to a single client can pollute requests dispatched from other clients.
If you still wish to reuse the same CurlMulti object with each client, then you can add a listener to the
ServiceBuilder's service_builder.create_client event to inject a custom CurlMulti object into each client as it is
created.
$multi = new Guzzle\Http\Curl\CurlMulti();
$builder = Guzzle\Service\Builder\ServiceBuilder::factory('/path/to/config.json');
$builder->addListener('service_builder.create_client', function ($event) use ($multi) {
$event['client']->setCurlMulti($multi);
}
});
URLs no longer have a default path value of '/' if no path was specified.
Before:
$request = $client->get('http://www.foo.com');
echo $request->getUrl();
// >> http://www.foo.com/
After:
$request = $client->get('http://www.foo.com');
echo $request->getUrl();
// >> http://www.foo.com
The exception message for Guzzle\Http\Exception\BadResponseException no longer contains the full HTTP request and
response information. You can, however, get access to the request and response object by calling getRequest() or
getResponse() on the exception object.
Multi-valued query parameters are no longer aggregated using a callback function. Guzzle\Http\Query now has a
setAggregator() method that accepts a Guzzle\Http\QueryAggregator\QueryAggregatorInterface object. This object is
responsible for handling the aggregation of multi-valued query string variables into a flattened hash.
Change \Guzzle\Service\Inspector::fromConfig to \Guzzle\Common\Collection::fromConfig
Before
use Guzzle\Service\Inspector;
class YourClient extends \Guzzle\Service\Client
{
public static function factory($config = array())
{
$default = array();
$required = array('base_url', 'username', 'api_key');
$config = Inspector::fromConfig($config, $default, $required);
$client = new self(
$config->get('base_url'),
$config->get('username'),
$config->get('api_key')
);
$client->setConfig($config);
$client->setDescription(ServiceDescription::factory(__DIR__ . DIRECTORY_SEPARATOR . 'client.json'));
return $client;
}
After
use Guzzle\Common\Collection;
class YourClient extends \Guzzle\Service\Client
{
public static function factory($config = array())
{
$default = array();
$required = array('base_url', 'username', 'api_key');
$config = Collection::fromConfig($config, $default, $required);
$client = new self(
$config->get('base_url'),
$config->get('username'),
$config->get('api_key')
);
$client->setConfig($config);
$client->setDescription(ServiceDescription::factory(__DIR__ . DIRECTORY_SEPARATOR . 'client.json'));
return $client;
}
Before
<?xml version="1.0" encoding="UTF-8"?>
<client>
<commands>
<!-- Groups -->
<command name="list_groups" method="GET" uri="groups.json">
<doc>Get a list of groups</doc>
</command>
<command name="search_groups" method="GET" uri='search.json?query="{{query}} type:group"'>
<doc>Uses a search query to get a list of groups</doc>
<param name="query" type="string" required="true" />
</command>
<command name="create_group" method="POST" uri="groups.json">
<doc>Create a group</doc>
<param name="data" type="array" location="body" filters="json_encode" doc="Group JSON"/>
<param name="Content-Type" location="header" static="application/json"/>
</command>
<command name="delete_group" method="DELETE" uri="groups/{{id}}.json">
<doc>Delete a group by ID</doc>
<param name="id" type="integer" required="true"/>
</command>
<command name="get_group" method="GET" uri="groups/{{id}}.json">
<param name="id" type="integer" required="true"/>
</command>
<command name="update_group" method="PUT" uri="groups/{{id}}.json">
<doc>Update a group</doc>
<param name="id" type="integer" required="true"/>
<param name="data" type="array" location="body" filters="json_encode" doc="Group JSON"/>
<param name="Content-Type" location="header" static="application/json"/>
</command>
</commands>
</client>
After
{
"name": "Zendesk REST API v2",
"apiVersion": "2012-12-31",
"description":"Provides access to Zendesk views, groups, tickets, ticket fields, and users",
"operations": {
"list_groups": {
"httpMethod":"GET",
"uri": "groups.json",
"summary": "Get a list of groups"
},
"search_groups":{
"httpMethod":"GET",
"uri": "search.json?query=\"{query} type:group\"",
"summary": "Uses a search query to get a list of groups",
"parameters":{
"query":{
"location": "uri",
"description":"Zendesk Search Query",
"type": "string",
"required": true
}
}
},
"create_group": {
"httpMethod":"POST",
"uri": "groups.json",
"summary": "Create a group",
"parameters":{
"data": {
"type": "array",
"location": "body",
"description":"Group JSON",
"filters": "json_encode",
"required": true
},
"Content-Type":{
"type": "string",
"location":"header",
"static": "application/json"
}
}
},
"delete_group": {
"httpMethod":"DELETE",
"uri": "groups/{id}.json",
"summary": "Delete a group",
"parameters":{
"id":{
"location": "uri",
"description":"Group to delete by ID",
"type": "integer",
"required": true
}
}
},
"get_group": {
"httpMethod":"GET",
"uri": "groups/{id}.json",
"summary": "Get a ticket",
"parameters":{
"id":{
"location": "uri",
"description":"Group to get by ID",
"type": "integer",
"required": true
}
}
},
"update_group": {
"httpMethod":"PUT",
"uri": "groups/{id}.json",
"summary": "Update a group",
"parameters":{
"id": {
"location": "uri",
"description":"Group to update by ID",
"type": "integer",
"required": true
},
"data": {
"type": "array",
"location": "body",
"description":"Group JSON",
"filters": "json_encode",
"required": true
},
"Content-Type":{
"type": "string",
"location":"header",
"static": "application/json"
}
}
}
}
Commands are now called Operations
Before
use Guzzle\Service\Description\ServiceDescription;
$sd = new ServiceDescription();
$sd->getCommands(); // @returns ApiCommandInterface[]
$sd->hasCommand($name);
$sd->getCommand($name); // @returns ApiCommandInterface|null
$sd->addCommand($command); // @param ApiCommandInterface $command
After
use Guzzle\Service\Description\ServiceDescription;
$sd = new ServiceDescription();
$sd->getOperations(); // @returns OperationInterface[]
$sd->hasOperation($name);
$sd->getOperation($name); // @returns OperationInterface|null
$sd->addOperation($operation); // @param OperationInterface $operation
Namespace is now Guzzle\Inflection\Inflector
Namespace is now Guzzle\Plugin. Many other changes occur within this namespace and are detailed in their own sections below.
Now Guzzle\Plugin\Log\LogPlugin and Guzzle\Log respectively.
Before
use Guzzle\Common\Log\ClosureLogAdapter;
use Guzzle\Http\Plugin\LogPlugin;
/** @var \Guzzle\Http\Client */
$client;
// $verbosity is an integer indicating desired message verbosity level
$client->addSubscriber(new LogPlugin(new ClosureLogAdapter(function($m) { echo $m; }, $verbosity = LogPlugin::LOG_VERBOSE);
After
use Guzzle\Log\ClosureLogAdapter;
use Guzzle\Log\MessageFormatter;
use Guzzle\Plugin\Log\LogPlugin;
/** @var \Guzzle\Http\Client */
$client;
// $format is a string indicating desired message format -- @see MessageFormatter
$client->addSubscriber(new LogPlugin(new ClosureLogAdapter(function($m) { echo $m; }, $format = MessageFormatter::DEBUG_FORMAT);
Now Guzzle\Plugin\CurlAuth\CurlAuthPlugin.
Now Guzzle\Plugin\Backoff\BackoffPlugin, and other changes.
Before
use Guzzle\Http\Plugin\ExponentialBackoffPlugin;
$backoffPlugin = new ExponentialBackoffPlugin($maxRetries, array_merge(
ExponentialBackoffPlugin::getDefaultFailureCodes(), array(429)
));
$client->addSubscriber($backoffPlugin);
After
use Guzzle\Plugin\Backoff\BackoffPlugin;
use Guzzle\Plugin\Backoff\HttpBackoffStrategy;
// Use convenient factory method instead -- see implementation for ideas of what
// you can do with chaining backoff strategies
$backoffPlugin = BackoffPlugin::getExponentialBackoff($maxRetries, array_merge(
HttpBackoffStrategy::getDefaultFailureCodes(), array(429)
));
$client->addSubscriber($backoffPlugin);
(See #217) (Fixed in 09daeb8c666fb44499a0646d655a8ae36456575e)
In version 2.8 setting the Accept-Encoding header would set the CURLOPT_ENCODING option, which permitted cURL to
properly handle gzip/deflate compressed responses from the server. In versions affected by this bug this does not happen.
See issue #217 for a workaround, or use a version containing the fix.