diff --git a/docs/en/appendices/6-0-migration-guide.md b/docs/en/appendices/6-0-migration-guide.md
index 42d4306bb4..482b2039a8 100644
--- a/docs/en/appendices/6-0-migration-guide.md
+++ b/docs/en/appendices/6-0-migration-guide.md
@@ -150,6 +150,59 @@ Some properties have also been renamed to better reflect their purpose. These ar
### Http
+CakePHP now provides its own PSR-7 HTTP message implementations and no longer
+requires `laminas/laminas-diactoros`. Replace direct uses of the following
+Diactoros classes with their CakePHP equivalents, including in test fixtures:
+
+| Previous class | Replacement |
+|-----------------------------------------------|---------------------------------------|
+| `Laminas\Diactoros\Uri` | `Cake\Http\Uri` |
+| `Laminas\Diactoros\Stream` | `Cake\Http\Stream` |
+| `Laminas\Diactoros\UploadedFile` | `Cake\Http\UploadedFile` |
+| `Laminas\Diactoros\Response\RedirectResponse` | `Cake\Http\Response\RedirectResponse` |
+| `Laminas\Diactoros\MessageTrait` | `Cake\Http\MessageTrait` |
+
+CakePHP's PSR-17 factories now create CakePHP objects. Type against the PSR
+interfaces when your code accepts multiple implementations. If your application
+still uses other Diactoros classes or functions, declare
+`laminas/laminas-diactoros` as a direct dependency. CakePHP also no longer requires
+`laminas/laminas-httphandlerrunner`; declare it directly if your application uses it.
+
+- `RequestFactory::createRequest()` now declares `Cake\Http\Client\Request`
+ as its return type. `ResponseFactory::createResponse()` now declares
+ `Cake\Http\Response`. The stream, URI, and uploaded-file factories return their
+ corresponding CakePHP classes. Update overridden methods to use compatible
+ return types.
+- Custom HTTP client authentication strategies must implement
+ `Cake\Http\Client\Auth\AuthenticationInterface`. Strategies used with the
+ `proxy` option must also implement
+ `Cake\Http\Client\Auth\ProxyAuthenticationInterface`. Update overrides of
+ `Client::createAuth()` to return `AuthenticationInterface`. See
+ [Custom Authentication Strategies](../core-libraries/httpclient#custom-authentication-strategies).
+- `Client::createRequest()` now accepts `array|string|null` for its `$data`
+ argument. Update the signatures of overrides accordingly.
+- When following a `303` redirect, the HTTP client now changes non-`HEAD`
+ requests to `GET` and removes the request body and its associated headers.
+ Redirects to a different scheme, host, or port remove the original
+ `Authorization` and `Cookie` headers; matching cookies from the client's cookie
+ jar are then applied. Relative redirect locations are resolved against the
+ current request URL. Redirect targets must use HTTP or HTTPS and have a host.
+- `Client\Response` now defaults to status `200` when constructed without a
+ status line. Its body constructor argument accepts a string or
+ `Psr\Http\Message\StreamInterface`; a supplied stream is used as-is.
+- HTTP message header mutations validate header names and values. Invalid names,
+ control characters in values, and empty value arrays throw
+ `InvalidArgumentException`. Use `withoutHeader()` to remove a header.
+- When the HTTP client decodes a gzip response body, it removes the
+ `Content-Encoding` and `Content-Length` headers so they describe the decoded
+ body. Invalid gzip data raises `Cake\Core\Exception\CakeException`.
+- `CallbackStream` no longer extends the Diactoros implementation. Its callback
+ runs once when the contents are read, and subsequent reads return an empty
+ string. It cannot be rewound or read in chunks using `read()`.
+- `JsonStreamResponse` validates its options when constructed and when calling
+ `withStreamOptions()`. Invalid option types or values, such as an unsupported
+ `format` or a non-positive `flushEvery`, throw `InvalidArgumentException`.
+
- Using `$request->getParam('?')` to get the query params is no longer possible.
Use `$request->getQueryParams()` instead.
- The methods `_getCookies()`, `_getJson()`, `_getXml()`, `_getHeaders()`
@@ -209,6 +262,15 @@ Some properties have also been renamed to better reflect their purpose. These ar
## New Features
+### Http
+
+- The HTTP client supports live response bodies through the Stream adapter with
+ the `stream` and `readTimeout` options. See
+ [Streaming Responses](../core-libraries/httpclient#streaming-responses).
+- `Client\Response::getJsonArray()` provides strict JSON array decoding, and
+ `getJsonAs()` passes decoded data to an application-defined validating decoder.
+ See [Reading JSON and XML Response Bodies](../core-libraries/httpclient#http-client-xml-json).
+
### Event
- Event subscriptions can now be declared on listener classes and methods with
diff --git a/docs/en/controllers/request-response.md b/docs/en/controllers/request-response.md
index b0403d28a5..24c65025e1 100644
--- a/docs/en/controllers/request-response.md
+++ b/docs/en/controllers/request-response.md
@@ -10,6 +10,11 @@ responses. The request object in CakePHP allows you to introspect an incoming
request, while the response object allows you to effortlessly create HTTP
responses from your controllers.
+CakePHP provides its own PSR-7 URI, stream, and uploaded-file implementations in
+`Cake\Http`. The PSR-17 factories, such as `UriFactory`, `StreamFactory`, and
+`UploadedFileFactory`, create these CakePHP objects. You can also use other
+implementations of the corresponding PSR interfaces.
+
## Request
@@ -188,7 +193,7 @@ $attachment = $this->request->getData('attachment');
By default, file uploads are represented in the request data as objects that implement
[\Psr\Http\Message\UploadedFileInterface](https://www.php-fig.org/psr/psr-7/#16-uploaded-files). In the current
implementation, the `$attachment` variable in the above example would by default hold an instance of
-`\Laminas\Diactoros\UploadedFile`.
+`\Cake\Http\UploadedFile`.
Accessing the uploaded file details is fairly simple, here's how you can obtain the same data as provided by the old
style file upload array:
@@ -235,7 +240,7 @@ Returns all uploaded files in a normalized array structure. For the above exampl
```php
[
- 'attachment' => object(Laminas\Diactoros\UploadedFile) {
+ 'attachment' => object(Cake\Http\UploadedFile) {
// ...
}
]
@@ -250,14 +255,14 @@ replace all possibly existing uploaded files:
```php
$files = [
'MyModel' => [
- 'attachment' => new \Laminas\Diactoros\UploadedFile(
+ 'attachment' => new \Cake\Http\UploadedFile(
$streamOrFile,
$size,
$errorStatus,
$clientFilename,
$clientMediaType,
),
- 'anotherAttachment' => new \Laminas\Diactoros\UploadedFile(
+ 'anotherAttachment' => new \Cake\Http\UploadedFile(
'/tmp/hfz6dbn.tmp',
123,
\UPLOAD_ERR_OK,
@@ -668,7 +673,8 @@ $files[0]->moveTo($targetPath);
### Working with the Request URI
Requests contain a URI object, which contains methods for interacting with the
-requested URI:
+requested URI. By default this is a `Cake\Http\Uri` implementing
+`Psr\Http\Message\UriInterface`:
```php
// Get the URI
@@ -680,6 +686,19 @@ $query = $uri->getQuery();
$host = $uri->getHost();
```
+You can construct a `Cake\Http\Uri` from a string, a native
+`Uri\Rfc3986\Uri`, or a native `Uri\WhatWg\Url`:
+
+```php
+use Cake\Http\Uri;
+
+$uri = new Uri('https://example.com/articles?page=2');
+$request = $request->withUri($uri);
+```
+
+`Cake\Http\Uri` adapts PHP's native URI parsing to PSR-7. Use it when an API
+requires `UriInterface`; the native PHP objects do not implement that interface.
+
## Response
`class` Cake\\Http\\**Response**
@@ -1051,7 +1070,7 @@ public function export()
`method` Cake\\Http\\Response::**withBody**(StreamInterface $body): static
To set the response body, use the `withBody()` method, which is provided by the
-`Laminas\Diactoros\MessageTrait`:
+`Cake\Http\MessageTrait`:
```php
$response = $response->withBody($stream);
@@ -1060,11 +1079,11 @@ $response = $response->withBody($stream);
Be sure that `$stream` is a `Psr\Http\Message\StreamInterface` object.
See below on how to create a new stream.
-You can also stream responses from files using `Laminas\Diactoros\Stream` streams:
+You can also stream responses from files using `Cake\Http\Stream` streams:
```php
// To stream from a file
-use Laminas\Diactoros\Stream;
+use Cake\Http\Stream;
$stream = new Stream('/path/to/file', 'rb');
$response = $response->withBody($stream);
@@ -1088,6 +1107,65 @@ $stream = new CallbackStream(function () use ($img) {
$response = $response->withBody($stream);
```
+The callback runs once when the contents are read. The stream cannot be rewound
+or read in chunks using `read()`. Use a readable stream for incremental reads.
+
+### Relaying a Live HTTP Response
+
+You can attach a live [HTTP client response body](../core-libraries/httpclient#streaming-responses)
+to a server response to forward bytes as they arrive. For example, a controller
+action can relay server-sent events:
+
+```php
+public function events(): \Cake\Http\Response
+{
+ $http = new \Cake\Http\Client([
+ 'adapter' => \Cake\Http\Client\Adapter\Stream::class,
+ ]);
+ $upstream = $http->get('https://example.com/events', [], [
+ 'stream' => true,
+ 'readTimeout' => 60,
+ 'headers' => ['Accept' => 'text/event-stream'],
+ ]);
+
+ return $this->response
+ ->withStatus($upstream->getStatusCode())
+ ->withHeader('Content-Type', $upstream->getHeaderLine('Content-Type'))
+ ->withHeader('Cache-Control', 'no-cache')
+ ->withHeader('X-Accel-Buffering', 'no')
+ ->withBody($upstream->getBody());
+}
+```
+
+`ResponseEmitter` reads readable, non-seekable bodies incrementally, flushes each
+chunk when PHP output buffering allows it, and closes the body after emission,
+including when reading fails. Return the response without consuming or closing
+its body first. Custom live bodies must support blocking reads; a read that
+returns an empty string before EOF causes the emitter to throw `RuntimeException`.
+
+Configure your web server and proxy to allow streaming. Nested PHP output
+buffers can also delay delivery. When using a custom emitter, enable
+`new \Cake\Http\ResponseEmitter(flushOutputBuffers: true)` to remove inner output
+buffers before emitting a live body. Empty inner compression buffers are
+discarded; drain nonempty custom output handlers before calling the emitter.
+
+### Redirect Responses
+
+Use `Cake\Http\Response\RedirectResponse` to create a redirect in middleware or
+other code that creates responses directly:
+
+```php
+use Cake\Http\Response\RedirectResponse;
+
+return new RedirectResponse('/articles', 301, ['X-Reason' => 'moved']);
+```
+
+The constructor accepts a URI string or `Psr\Http\Message\UriInterface`, a status
+code (default `302`), and additional headers. The response has an empty body and
+no default `Content-Type` header. Its `Location` header is set from the URI
+argument. In controllers you can continue to use
+[`Controller::redirect()`](../controllers#redirecting-to-other-pages).
+
### Setting the Character Set
`method` Cake\\Http\\Response::**withCharset**(string $charset): static
diff --git a/docs/en/core-libraries/httpclient.md b/docs/en/core-libraries/httpclient.md
index aa5f2a4948..3f36138342 100644
--- a/docs/en/core-libraries/httpclient.md
+++ b/docs/en/core-libraries/httpclient.md
@@ -163,6 +163,10 @@ addition request information. The following keys can be used in `$options`:
- `ssl_verify_host` - defaults to `true`. Validate the SSL certificate against the host name.
- `ssl_cafile` - defaults to built in cafile. Overwrite to use custom CA bundles.
- `timeout` - Duration to wait before timing out in seconds.
+- `stream` - Return a live response body after receiving headers instead of
+ buffering the entire body. Defaults to `false`. Requires the Stream adapter.
+- `readTimeout` - Timeout in seconds for each live response body read. Defaults
+ to `timeout`; `0` disables the read timeout. Must be finite and non-negative.
- `type` - Send a request body in a custom content type. Requires `$data` to
either be a string, or the `_content` option to be set when doing GET
requests.
@@ -174,6 +178,21 @@ The options parameter is always the 3rd parameter in each of the HTTP methods.
They can also be used when constructing `Client` to create
[scoped clients](#http_client_scoped_client).
+## Following Redirects
+
+Set the `redirect` option to the maximum number of redirects to follow:
+
+```php
+$response = $http->get('https://example.com/old-path', [], ['redirect' => 3]);
+```
+
+Relative `Location` values are resolved against the current request URL.
+Redirect targets must use HTTP or HTTPS and have a host. When following a `303`
+response, non-`HEAD` requests become `GET` requests with no body or body headers.
+When the scheme, host, or port changes, the original `Authorization` and `Cookie`
+headers are removed. Cookies matching the destination are added from the client's
+cookie jar.
+
## Authentication
`Cake\Http\Client` supports a few different authentication systems. Different
@@ -259,11 +278,45 @@ $http = new Client([
$response = $http->get('https://example.com/api/profile/1');
```
+### Custom Authentication Strategies
+
+Custom strategies must implement
+`Cake\Http\Client\Auth\AuthenticationInterface`. The `authentication()` method
+receives a request and the authentication options, and must return the request
+with any authentication changes applied:
+
+```php
+namespace App\Http\Client\Auth;
+
+use Cake\Http\Client\Auth\AuthenticationInterface;
+use Cake\Http\Client\Request;
+
+class ApiKey implements AuthenticationInterface
+{
+ public function authentication(Request $request, array $credentials): Request
+ {
+ return $request->withHeader('X-Api-Key', $credentials['key']);
+ }
+}
+```
+
+Select the strategy with the `auth.type` option:
+
+```php
+$response = $http->get('https://example.com/profile', [], [
+ 'auth' => ['type' => \App\Http\Client\Auth\ApiKey::class, 'key' => $apiKey],
+]);
+```
+
### Proxy Authentication
Some proxies require authentication to use them. Generally this authentication
-is Basic, but it can be implemented by any authentication adapter. By default
-Http\Client will assume Basic authentication, unless the type key is set:
+is Basic. A custom proxy strategy must implement both
+`Cake\Http\Client\Auth\AuthenticationInterface` and
+`Cake\Http\Client\Auth\ProxyAuthenticationInterface`, providing a
+`proxyAuthentication(Request $request, array $credentials): Request` method.
+Using a strategy without proxy support throws `Cake\Core\Exception\CakeException`.
+By default, Http\Client uses Basic authentication unless the `type` key is set:
```php
$http = new Client();
@@ -427,13 +480,62 @@ while (!$stream->eof()) {
}
```
+### Streaming Responses
+
+By default the HTTP client buffers the response body before returning. To process
+an ongoing response, such as server-sent events (SSE), select the Stream adapter
+and enable the `stream` option:
+
+```php
+use Cake\Http\Client;
+use Cake\Http\Client\Adapter\Stream;
+
+$http = new Client(['adapter' => Stream::class]);
+$response = $http->get('https://example.com/events', [], [
+ 'stream' => true,
+ 'readTimeout' => 60,
+ 'headers' => ['Accept' => 'text/event-stream'],
+]);
+
+$body = $response->getBody();
+try {
+ while (!$body->eof()) {
+ $chunk = $body->read(8192);
+ // Process bytes incrementally; a chunk may contain only part of an event.
+ }
+} finally {
+ $body->close();
+}
+```
+
+Streaming requires PHP's `allow_url_fopen` setting. The Curl adapter rejects
+`stream => true`; explicitly select the Stream adapter even when the curl
+extension is installed.
+
+The client returns after the response headers arrive. The body is non-seekable,
+has no known size, and cannot be replayed. `readTimeout` controls individual body
+reads, defaults to `timeout`, and accepts `0` to disable the read timeout. Read
+failures and timeouts throw `Cake\Http\Client\Exception\NetworkException` while
+consuming the body, so handle them around the read loop as well as the request.
+
+`getStringBody()`, `getJson()`, `getJsonArray()`, `getJsonAs()` and `getXml()`
+consume a non-seekable body from its current position and wait for EOF. Avoid
+these methods for ongoing responses. `HttpClient.afterSend` runs before a live
+body has been consumed; logging and monitoring listeners should inspect headers
+and status without reading the body.
+
+To forward a live body from a controller action, attach it to the server response
+and return that response. See
+[Relaying a Live HTTP Response](../controllers/request-response#relaying-a-live-http-response).
+
### Reading JSON and XML Response Bodies
Since JSON and XML responses are commonly used, response objects provide a way
-to use accessors to read decoded data. JSON data is decoded into an array, while
-XML data is decoded into a `SimpleXMLElement` tree:
+to use accessors to read decoded data. JSON objects and arrays are decoded into
+PHP arrays; other JSON values retain their corresponding PHP types. XML data is
+decoded into a `SimpleXMLElement` tree:
```php
// Get some XML
@@ -448,7 +550,34 @@ $json = $response->getJson();
```
The decoded response data is stored in the response object, so accessing it
-multiple times has no additional cost.
+multiple times has no additional cost. Calling `withBody()` clears the cached
+JSON and XML values for the new response.
+
+`getJson()` returns `null` for both invalid JSON and the JSON value `null`. Use
+`getJsonArray()` when you require an array or `null` and want decoding failures
+to raise exceptions:
+
+```php
+$data = $response->getJsonArray();
+```
+
+JSON objects and arrays are returned as PHP arrays, and JSON `null` is returned
+as `null`. Invalid JSON throws `JsonException`. Valid scalar values, such as
+`true`, `42`, or `"text"`, throw `UnexpectedValueException`.
+
+Use `getJsonAs()` to validate the decoded array and convert it into an
+application-specific value. The decoder receives the result of `getJsonArray()`
+and is responsible for validating its shape:
+
+```php
+$name = $response->getJsonAs(function (?array $data): string {
+ if (!isset($data['name']) || !is_string($data['name'])) {
+ throw new \UnexpectedValueException('Expected a string name.');
+ }
+
+ return $data['name'];
+});
+```
### Accessing Response Headers
diff --git a/docs/en/development/testing.md b/docs/en/development/testing.md
index d51bc5e4e1..04c17dbad0 100644
--- a/docs/en/development/testing.md
+++ b/docs/en/development/testing.md
@@ -1390,7 +1390,7 @@ Simulating file uploads is straightforward when you use the default
create instances that implement
[\Psr\Http\Message\UploadedFileInterface](https://www.php-fig.org/psr/psr-7/#16-uploaded-files)
(the default implementation currently used by CakePHP is
-`\Laminas\Diactoros\UploadedFile`), and pass them in your test request data.
+`\Cake\Http\UploadedFile`), and pass them in your test request data.
In the CLI environment such objects will by default pass validation checks that
test whether the file was uploaded via HTTP. The same is not true for array style
data as found in `$_FILES`, it would fail that check.
@@ -1423,7 +1423,7 @@ The test that would simulate the corresponding request could look like this:
```php
public function testAddWithUploads(): void
{
- $teaserImage = new \Laminas\Diactoros\UploadedFile(
+ $teaserImage = new \Cake\Http\UploadedFile(
'/path/to/test/file.jpg', // stream or path to file representing the temp file
12345, // the filesize in bytes
\UPLOAD_ERR_OK, // the upload/error status
@@ -1431,7 +1431,7 @@ public function testAddWithUploads(): void
'image/jpeg', // the mimetype as sent by the client
);
- $textAttachment = new \Laminas\Diactoros\UploadedFile(
+ $textAttachment = new \Cake\Http\UploadedFile(
'/path/to/test/file.txt',
12345,
\UPLOAD_ERR_OK,
@@ -1439,7 +1439,7 @@ public function testAddWithUploads(): void
'text/plain',
);
- $pdfAttachment = new \Laminas\Diactoros\UploadedFile(
+ $pdfAttachment = new \Cake\Http\UploadedFile(
'/path/to/test/file.pdf',
12345,
\UPLOAD_ERR_OK,
@@ -1498,7 +1498,7 @@ or otherwise invalid files that do not pass validation:
```php
public function testAddWithInvalidUploads(): void
{
- $missingTeaserImageUpload = new \Laminas\Diactoros\UploadedFile(
+ $missingTeaserImageUpload = new \Cake\Http\UploadedFile(
'',
0,
\UPLOAD_ERR_NO_FILE,
@@ -1506,7 +1506,7 @@ public function testAddWithInvalidUploads(): void
'',
);
- $uploadFailureAttachment = new \Laminas\Diactoros\UploadedFile(
+ $uploadFailureAttachment = new \Cake\Http\UploadedFile(
'/path/to/test/file.txt',
1234567890,
\UPLOAD_ERR_INI_SIZE,
@@ -1514,7 +1514,7 @@ public function testAddWithInvalidUploads(): void
'text/plain',
);
- $invalidTypeAttachment = new \Laminas\Diactoros\UploadedFile(
+ $invalidTypeAttachment = new \Cake\Http\UploadedFile(
'/path/to/test/file.exe',
12345,
\UPLOAD_ERR_OK,