Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,22 @@

## Code and tooling

- All code must remain compatible with PHP 7.4, and PHPStan and PHP-CS-Fixer
must be run against PHP 7.4.
- All code must remain compatible with PHP 7.4. Run PHPStan and PHP-CS-Fixer
only under a PHP 7.4.x runtime; never run either tool under any other PHP
major.minor version.
- Use fully qualified `#[\SensitiveParameter]` on concrete executable
parameters when their established role normally carries a secret,
credential-bearing aggregate, or confidential Guzzle-owned container, and
the active frame can throw or invoke throwing code.
- Repeat the attribute on every qualifying owned caller, callee, concrete trait
method, and closure parameter. Do not add it to interfaces, abstract-only
declarations, pure/no-realistic-throw helpers, assignment-only sites,
arbitrary generic payloads, or completed non-recoverable derivatives.
- For PHP 7.4 compatibility, put `#[\SensitiveParameter]` on its own line and
the parameter on the following line, expand the complete parameter list, and
never add a comma after the final parameter. Native trace redaction starts on
PHP 8.2 and does not redact logs, messages, properties, wire traffic, captured
variables, return values, or the separate backtrace `$this`/`object`.
- Always pass an explicit character list to `trim()`, `ltrim()`, and `rtrim()`;
never rely on the default characters.
- Handle `preg_*` engine failures: when the result is used as data, test for
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Require service client response transformers to return `ResultInterface` values
* Require command collections to be iterable and reject single commands in `executeAll()` and `executeAllAsync()`
* Require custom middleware, transformers, and `executeAll()` callbacks to accept exact argument types instead of relying on scalar coercion
* Redact credential-bearing command and HTTP arguments from PHP 8.2+ backtraces
* Preserve requests from PSR-18 request and network exceptions and from Guzzle transfer exceptions when wrapping command failures
* Allow command exception constructors to accept any previous `Throwable`
* Add generic PHPDoc return types to asynchronous service client APIs
Expand Down
12 changes: 12 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ userland callbacks continue to work at runtime when PHP accepts them.
`ServiceClient` no longer supports native PHP `serialize()` or `unserialize()`.
Persist command names and parameter arrays instead of runtime client objects.

#### Sensitive Parameters and Backtraces

Guzzle Command 2.0 marks credential-bearing command, request, response, and
exception parameters as sensitive. On PHP 8.2 and later, exception traces and
backtraces replace those argument values with `SensitiveParameterValue`
objects. PHP 7.4 through 8.1 retain the original trace arguments.

This does not redact exception messages, application logs, HTTP traffic,
properties, captured variables, return values, or the separate `$this` object
in an explicit backtrace. Custom middleware and transformers must mark their
own sensitive parameters.

1.0 from 0.8
------------

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"guzzlehttp/promises": "^3.0@dev",
"guzzlehttp/psr7": "^3.0@dev",
"psr/http-client": "^1.0",
"symfony/polyfill-php80": "^1.25"
"symfony/polyfill-php80": "^1.25",
"symfony/polyfill-php82": "^1.27"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
Expand Down
8 changes: 6 additions & 2 deletions src/Exception/CommandException.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ class CommandException extends \RuntimeException implements GuzzleException

private ?ResponseInterface $response;

public static function fromPrevious(CommandInterface $command, \Exception $prev): self
{
public static function fromPrevious(
#[\SensitiveParameter]
CommandInterface $command,
#[\SensitiveParameter]
\Exception $prev
): self {
// If the exception is already a command exception, return it.
if ($prev instanceof self && $command === $prev->getCommand()) {
return $prev;
Expand Down
54 changes: 39 additions & 15 deletions src/ServiceClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,28 @@ public function getHandlerStack(): HandlerStack
return $this->handlerStack;
}

public function getCommand(string $name, array $params = []): CommandInterface
{
public function getCommand(
string $name,
#[\SensitiveParameter]
array $params = []
): CommandInterface {
return new Command($name, $params, clone $this->handlerStack);
}

public function execute(CommandInterface $command): ResultInterface
{
public function execute(
#[\SensitiveParameter]
CommandInterface $command
): ResultInterface {
return $this->executeAsync($command)->wait();
}

/**
* @return PromiseInterface<ResultInterface, mixed>
*/
public function executeAsync(CommandInterface $command): PromiseInterface
{
public function executeAsync(
#[\SensitiveParameter]
CommandInterface $command
): PromiseInterface {
$stack = $command->getHandlerStack() ?: $this->handlerStack;

/** @var callable(CommandInterface): PromiseInterface<ResultInterface, mixed> $handler */
Expand All @@ -103,8 +110,11 @@ public function executeAsync(CommandInterface $command): PromiseInterface
*
* @return array<array-key, mixed>
*/
public function executeAll(iterable $commands, array $options = []): array
{
public function executeAll(
#[\SensitiveParameter]
iterable $commands,
array $options = []
): array {
$fulfilled = $options['fulfilled'] ?? null;
$rejected = $options['rejected'] ?? null;

Expand Down Expand Up @@ -145,8 +155,11 @@ public function executeAll(iterable $commands, array $options = []): array
*
* @return PromiseInterface<mixed, mixed>
*/
public function executeAllAsync(iterable $commands, array $options = []): PromiseInterface
{
public function executeAllAsync(
#[\SensitiveParameter]
iterable $commands,
array $options = []
): PromiseInterface {
// A single command satisfies the iterable type but would be iterated
// as a collection of its own parameters.
if ($commands instanceof CommandInterface) {
Expand Down Expand Up @@ -183,8 +196,11 @@ public function executeAllAsync(iterable $commands, array $options = []): Promis
*
* @see ServiceClientInterface::getCommand
*/
public function __call(string $name, array $args)
{
public function __call(
string $name,
#[\SensitiveParameter]
array $args
) {
$args = isset($args[0]) ? $args[0] : [];
if (str_ends_with($name, 'Async')) {
$command = $this->getCommand(substr($name, 0, -5), $args);
Expand All @@ -202,7 +218,10 @@ public function __call(string $name, array $args)
*/
private function createCommandHandler(): callable
{
return function (CommandInterface $command): PromiseInterface {
return function (
#[\SensitiveParameter]
CommandInterface $command
): PromiseInterface {
return Promise\Coroutine::of(function () use ($command): \Generator {
// Prepare the HTTP options.
$opts = $command['@http'] ?: [];
Expand All @@ -226,8 +245,10 @@ private function createCommandHandler(): callable
/**
* Transforms a Command object into a Request object.
*/
private function transformCommandToRequest(CommandInterface $command): RequestInterface
{
private function transformCommandToRequest(
#[\SensitiveParameter]
CommandInterface $command
): RequestInterface {
$transform = $this->commandToRequestTransformer;

return $transform($command);
Expand All @@ -238,8 +259,11 @@ private function transformCommandToRequest(CommandInterface $command): RequestIn
* into a Result object.
*/
private function transformResponseToResult(
#[\SensitiveParameter]
ResponseInterface $response,
#[\SensitiveParameter]
RequestInterface $request,
#[\SensitiveParameter]
CommandInterface $command
): ResultInterface {
$transform = $this->responseToResultTransformer;
Expand Down