Skip to content

Commit 6c39723

Browse files
JasonTheAdamsJasonTheAdamsfelixarntz
authored
Merge pull request #109 from WordPress/http-request-options
Co-authored-by: JasonTheAdams <[email protected]> Co-authored-by: felixarntz <[email protected]>
2 parents a447c4e + b976abd commit 6c39723

File tree

11 files changed

+1162
-15
lines changed

11 files changed

+1162
-15
lines changed

docs/ARCHITECTURE.md

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -794,27 +794,43 @@ This section describes the HTTP communication architecture that differs from the
794794
4. **PSR Compliance**: The transporter uses PSR-7 (HTTP messages), PSR-17 (HTTP factories), and PSR-18 (HTTP client) internally
795795
5. **No Direct Coupling**: The library remains decoupled from any specific HTTP client implementation
796796
6. **Provider Domain Location**: HTTP components are located within the Providers domain (`src/Providers/Http/`) as they are provider-specific infrastructure
797-
7. **Synchronous Only**: Currently supports only synchronous HTTP requests. Async support may be added in the future if needed
797+
7. **Per-request Transport Options**: Request-specific transport settings flow through a `RequestOptions` DTO, allowing callers to control timeouts and redirect handling on a per-request basis
798+
8. **Extensible Client Support**: HTTP clients can opt into receiving request options by implementing `ClientWithOptionsInterface`, and the transporter automatically bridges well-known client shapes such as Guzzle's `send($request, array $options)` signature
799+
9. **Synchronous Only**: Currently supports only synchronous HTTP requests. Async support may be added in the future if needed
798800

799801
### HTTP Communication Flow
800802

801803
```mermaid
802804
sequenceDiagram
803805
participant Model
804806
participant HttpTransporter
807+
participant RequestOptions
805808
participant PSR17Factory
806-
participant PSR18Client
807-
808-
Model->>HttpTransporter: send(Request)
809+
participant Client
810+
811+
Model->>HttpTransporter: send(Request, ?RequestOptions)
812+
HttpTransporter-->>RequestOptions: buildOptions(Request)
809813
HttpTransporter->>PSR17Factory: createRequest(Request)
810814
PSR17Factory-->>HttpTransporter: PSR-7 Request
811-
HttpTransporter->>PSR18Client: sendRequest(PSR-7 Request)
812-
PSR18Client-->>HttpTransporter: PSR-7 Response
815+
alt Client implements ClientWithOptionsInterface
816+
HttpTransporter->>Client: sendRequestWithOptions(PSR-7 Request, RequestOptions)
817+
else Client has Guzzle send signature
818+
HttpTransporter->>Client: send(PSR-7 Request, guzzleOptions)
819+
else Plain PSR-18 client
820+
HttpTransporter->>Client: sendRequest(PSR-7 Request)
821+
end
822+
Client-->>HttpTransporter: PSR-7 Response
813823
HttpTransporter->>PSR17Factory: parseResponse(PSR-7 Response)
814824
PSR17Factory-->>HttpTransporter: Response
815825
HttpTransporter-->>Model: Response
816826
```
817827

828+
Whenever request options are present, the transporter enriches the PSR-18 call path: it translates the `RequestOptions` DTO into the client’s native format. Clients that implement `ClientWithOptionsInterface` receive the DTO directly, while Guzzle-style clients are detected through reflection and receive an options array (e.g., `timeout`, `connect_timeout`, `allow_redirects`).
829+
830+
### ClientWithOptionsInterface
831+
832+
`ClientWithOptionsInterface` is a lightweight extension point for HTTP clients that already support per-request configuration. By implementing it, a client (for example, a wrapper around Guzzle or the WordPress AI Client’s richer transporter) can accept a `RequestOptions` instance directly through `sendRequestWithOptions()`. The transporter prefers this pathway, falling back to Guzzle detection or plain PSR-18 `sendRequest()` when the interface is not implemented, keeping the core agnostic while still allowing rich integrations.
833+
818834

819835
### Details: Class diagram for AI extenders
820836

@@ -889,7 +905,10 @@ direction LR
889905
890906
namespace AiClientNamespace.Providers.Http.Contracts {
891907
class HttpTransporterInterface {
892-
+send(Request $request) Response
908+
+send(Request $request, ?RequestOptions $options) Response
909+
}
910+
interface ClientWithOptionsInterface {
911+
+sendRequestWithOptions(RequestInterface $request, RequestOptions $options) ResponseInterface
893912
}
894913
class RequestAuthenticationInterface {
895914
+authenticateRequest(Request $request) Request
@@ -912,6 +931,24 @@ direction LR
912931
+getHeaders() array< string, string[] >
913932
+getBody() ?string
914933
+getData() ?array< string, mixed >
934+
+getOptions() ?RequestOptions
935+
+withHeader(string $name, string|list< string > $value) self
936+
+withData(string|array< string, mixed > $data) self
937+
+withOptions(?RequestOptions $options) self
938+
+toArray() array< string, mixed >
939+
+getJsonSchema() array< string, mixed >$
940+
+fromArray(array< string, mixed > $array) self$
941+
+fromPsrRequest(RequestInterface $psrRequest) self$
942+
}
943+
class RequestOptions {
944+
+setTimeout(?float $timeout) void
945+
+setConnectTimeout(?float $timeout) void
946+
+setMaxRedirects(?int $maxRedirects) void
947+
+getTimeout() ?float
948+
+getConnectTimeout() ?float
949+
+allowsRedirects() ?bool
950+
+getMaxRedirects() ?int
951+
+toArray() array< string, mixed >
915952
+getJsonSchema() array< string, mixed >$
916953
}
917954
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WordPress\AiClient\Providers\Http\Contracts;
6+
7+
use Psr\Http\Message\RequestInterface;
8+
use Psr\Http\Message\ResponseInterface;
9+
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
10+
11+
/**
12+
* Interface for HTTP clients that support per-request transport options.
13+
*
14+
* Extends the capabilities of PSR-18 clients by allowing custom transport
15+
* configuration such as timeouts and redirect handling on each request.
16+
*
17+
* @since n.e.x.t
18+
*/
19+
interface ClientWithOptionsInterface
20+
{
21+
/**
22+
* Sends an HTTP request with the given transport options.
23+
*
24+
* @since n.e.x.t
25+
*
26+
* @param RequestInterface $request The PSR-7 request to send.
27+
* @param RequestOptions $options The request transport options. Must not be null.
28+
* @return ResponseInterface The PSR-7 response received.
29+
*/
30+
public function sendRequestWithOptions(
31+
RequestInterface $request,
32+
RequestOptions $options
33+
): ResponseInterface;
34+
}

src/Providers/Http/Contracts/HttpTransporterInterface.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace WordPress\AiClient\Providers\Http\Contracts;
66

77
use WordPress\AiClient\Providers\Http\DTO\Request;
8+
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
89
use WordPress\AiClient\Providers\Http\DTO\Response;
910

1011
/**
@@ -23,7 +24,8 @@ interface HttpTransporterInterface
2324
* @since 0.1.0
2425
*
2526
* @param Request $request The request to send.
27+
* @param RequestOptions|null $options Optional transport options for the request.
2628
* @return Response The response received.
2729
*/
28-
public function send(Request $request): Response;
30+
public function send(Request $request, ?RequestOptions $options = null): Response;
2931
}

src/Providers/Http/DTO/Request.php

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
*
2020
* @since 0.1.0
2121
*
22+
* @phpstan-import-type RequestOptionsArrayShape from RequestOptions
2223
* @phpstan-type RequestArrayShape array{
2324
* method: string,
2425
* uri: string,
2526
* headers: array<string, list<string>>,
26-
* body?: string|null
27+
* body?: string|null,
28+
* options?: RequestOptionsArrayShape
2729
* }
2830
*
2931
* @extends AbstractDataTransferObject<RequestArrayShape>
@@ -34,6 +36,7 @@ class Request extends AbstractDataTransferObject
3436
public const KEY_URI = 'uri';
3537
public const KEY_HEADERS = 'headers';
3638
public const KEY_BODY = 'body';
39+
public const KEY_OPTIONS = 'options';
3740

3841
/**
3942
* @var HttpMethodEnum The HTTP method.
@@ -60,6 +63,11 @@ class Request extends AbstractDataTransferObject
6063
*/
6164
protected ?string $body = null;
6265

66+
/**
67+
* @var RequestOptions|null Request transport options.
68+
*/
69+
protected ?RequestOptions $options = null;
70+
6371
/**
6472
* Constructor.
6573
*
@@ -69,11 +77,17 @@ class Request extends AbstractDataTransferObject
6977
* @param string $uri The request URI.
7078
* @param array<string, string|list<string>> $headers The request headers.
7179
* @param string|array<string, mixed>|null $data The request data.
80+
* @param RequestOptions|null $options The request transport options.
7281
*
7382
* @throws InvalidArgumentException If the URI is empty.
7483
*/
75-
public function __construct(HttpMethodEnum $method, string $uri, array $headers = [], $data = null)
76-
{
84+
public function __construct(
85+
HttpMethodEnum $method,
86+
string $uri,
87+
array $headers = [],
88+
$data = null,
89+
?RequestOptions $options = null
90+
) {
7791
if (empty($uri)) {
7892
throw new InvalidArgumentException('URI cannot be empty.');
7993
}
@@ -88,6 +102,8 @@ public function __construct(HttpMethodEnum $method, string $uri, array $headers
88102
} elseif (is_array($data)) {
89103
$this->data = $data;
90104
}
105+
106+
$this->options = $options;
91107
}
92108

93109
/**
@@ -281,6 +297,33 @@ public function getData(): ?array
281297
return $this->data;
282298
}
283299

300+
/**
301+
* Gets the request options.
302+
*
303+
* @since n.e.x.t
304+
*
305+
* @return RequestOptions|null Request transport options when configured.
306+
*/
307+
public function getOptions(): ?RequestOptions
308+
{
309+
return $this->options;
310+
}
311+
312+
/**
313+
* Returns a new instance with the specified request options.
314+
*
315+
* @since n.e.x.t
316+
*
317+
* @param RequestOptions|null $options The request options to apply.
318+
* @return self A new instance with the options.
319+
*/
320+
public function withOptions(?RequestOptions $options): self
321+
{
322+
$new = clone $this;
323+
$new->options = $options;
324+
return $new;
325+
}
326+
284327
/**
285328
* {@inheritDoc}
286329
*
@@ -311,6 +354,7 @@ public static function getJsonSchema(): array
311354
'type' => ['string'],
312355
'description' => 'The request body.',
313356
],
357+
self::KEY_OPTIONS => RequestOptions::getJsonSchema(),
314358
],
315359
'required' => [self::KEY_METHOD, self::KEY_URI, self::KEY_HEADERS],
316360
];
@@ -337,6 +381,13 @@ public function toArray(): array
337381
$array[self::KEY_BODY] = $body;
338382
}
339383

384+
if ($this->options !== null) {
385+
$optionsArray = $this->options->toArray();
386+
if (!empty($optionsArray)) {
387+
$array[self::KEY_OPTIONS] = $optionsArray;
388+
}
389+
}
390+
340391
return $array;
341392
}
342393

@@ -353,7 +404,10 @@ public static function fromArray(array $array): self
353404
HttpMethodEnum::from($array[self::KEY_METHOD]),
354405
$array[self::KEY_URI],
355406
$array[self::KEY_HEADERS] ?? [],
356-
$array[self::KEY_BODY] ?? null
407+
$array[self::KEY_BODY] ?? null,
408+
isset($array[self::KEY_OPTIONS])
409+
? RequestOptions::fromArray($array[self::KEY_OPTIONS])
410+
: null
357411
);
358412
}
359413

0 commit comments

Comments
 (0)