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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
- Handle `preg_*` engine failures: when the result is used as data, test for
`false` or `null` and throw a `\RuntimeException` including
`preg_last_error_msg()`; boolean validation guards must compare strictly, such
as `=== 1`, so an engine failure can only ever fail closed.
as `=== 1`, so an engine failure can only ever fail closed. Diagnostic
escaping is the narrow exception: use a deterministic bytewise fallback rather
than throwing, so it cannot obscure the original exception.
- Anchor validation patterns to the true end of input with the `D` modifier or
`\z`; a bare `$` accepts a trailing newline.
- Never embed raw control bytes in exception messages and other diagnostics;
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Drop support for PHP 7.2 and 7.3
* Require `guzzlehttp/guzzle` ^8.0, `guzzlehttp/promises` ^3.0, and `guzzlehttp/psr7` ^3.0
* Escape unsafe command names and previous messages in generated command exceptions
* Reject native PHP serialization of `ServiceClient`
* Require custom implementations and subclasses of public command APIs to match native method signatures
* Require service client response transformers to return `ResultInterface` values
Expand Down
4 changes: 2 additions & 2 deletions src/Exception/CommandException.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\ResponseException;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Psr7\DiagnosticValue;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
Expand Down Expand Up @@ -54,8 +55,7 @@ public static function fromPrevious(CommandInterface $command, \Exception $prev)
}

// Prepare the message.
$message = 'There was an error executing the '.$command->getName()
.' command: '.$prev->getMessage();
$message = \sprintf('There was an error executing the %s command: %s', DiagnosticValue::escape($command->getName()), DiagnosticValue::escape($prev->getMessage()));

// Create the exception.
return new $class($message, $command, $prev, $request, $response);
Expand Down
17 changes: 17 additions & 0 deletions tests/Exception/CommandExceptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@ public function testFactoryReturnsExceptionIfAlreadyCommandException(): void
$this->assertSame($previous, $exception);
}

public function testFactoryEscapesUnsafeCommandAndPreviousMessages(): void
{
$commandName = "command\xC2\x80";
$previousMessage = "failure\xFF";
$command = $this->createMock(CommandInterface::class);
$command->method('getName')->willReturn($commandName);
$previous = new \RuntimeException($previousMessage);

$exception = CommandException::fromPrevious($command, $previous);

$this->assertSame('There was an error executing the command\\x80 command: failure\\xFF', $exception->getMessage());
$this->assertSame($command, $exception->getCommand());
$this->assertSame($commandName, $command->getName());
$this->assertSame($previous, $exception->getPrevious());
$this->assertSame($previousMessage, $previous->getMessage());
}

public function testFactoryReturnsClientExceptionFor400LevelStatusCode(): void
{
$command = $this->createMock(CommandInterface::class);
Expand Down