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
8 changes: 6 additions & 2 deletions packages/grpc/src/Metadata/Status.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ public function append(Metadata $md): Metadata
{
$md = $md
->replace(self::STATUS_HEADER, (string) $this->code->value)
->replace(self::MESSAGE_HEADER, $this->message ?? '');
->replace(self::MESSAGE_HEADER, (string) preg_replace_callback(
'/[^\x20-\x24\x26-\x7e]/',
static fn(array $byte): string => \sprintf('%%%02X', \ord($byte[0])),
$this->message ?? '',
));

if ($this->details !== null && $this->details !== '') {
$md = $md->replace(self::DETAILS_HEADER, $this->details);
Expand All @@ -46,7 +50,7 @@ function parseStatus(Metadata $md): Status

return new Status(
$code,
$md->value(Status::MESSAGE_HEADER),
($message = $md->value(Status::MESSAGE_HEADER)) !== null ? rawurldecode($message) : null,
$md->value(Status::DETAILS_HEADER),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,9 @@ public function handleRequest(Request $request): Response
return new Response(
status: HttpStatus::OK,
headers: $headers->kv,
trailers: new Trailers(Future::complete([
Metadata\Status::STATUS_HEADER => (string) Rpc\Code::UNIMPLEMENTED->value,
Metadata\Status::MESSAGE_HEADER => $e->getMessage(),
])),
trailers: new Trailers(Future::complete(
new Metadata()->withKey(new Metadata\Status(Rpc\Code::UNIMPLEMENTED, $e->getMessage()))->kv,
)),
);
}

Expand Down
89 changes: 89 additions & 0 deletions tests/Metadata/StatusTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace Thesis\Grpc\Metadata;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversFunction;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Thesis\Google\Rpc;
use Thesis\Grpc\Metadata;

#[CoversClass(Status::class)]
#[CoversFunction('Thesis\Grpc\Metadata\parseStatus')]
final class StatusTest extends TestCase
{
#[DataProvider('provideMessageCases')]
public function testMessage(string $message, string $headerValue): void
{
$md = new Metadata()->withKey(new Status(Rpc\Code::INTERNAL, $message));

self::assertSame($headerValue, $md->value(Status::MESSAGE_HEADER));
self::assertSame($message, parseStatus($md)->message);
}

/**
* @return iterable<array-key, array{string, string}>
*/
public static function provideMessageCases(): iterable
{
yield 'ascii' => [
'Unknown method Ping for service echos.api.v1.EchoService: [{}] ~!',
'Unknown method Ping for service echos.api.v1.EchoService: [{}] ~!',
];

yield 'percent' => [
'100%',
'100%25',
];

yield 'range boundaries' => [
" $%&~\x7f",
' $%25&~%7F',
];

yield 'special status message' => [
"\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n",
'%09%0Atest with whitespace%0D%0Aand Unicode BMP %E2%98%BA and non-BMP %F0%9F%98%88%09%0A',
];
}

#[DataProvider('provideParseMessageCases')]
public function testParseMessage(string $headerValue, string $message): void
{
self::assertSame($message, parseStatus(new Metadata()->with(Status::MESSAGE_HEADER, $headerValue))->message);
}

/**
* @return iterable<array-key, array{string, string}>
*/
public static function provideParseMessageCases(): iterable
{
yield 'lowercase hex' => [
'%e2%98%ba',
'☺',
];

yield 'invalid escape' => [
'%zz',
'%zz',
];

yield 'trailing percent' => [
'done 100%',
'done 100%',
];

yield 'plus is not a space' => [
'a+b',
'a+b',
];
}

public function testMissingMessage(): void
{
self::assertNull(parseStatus(new Metadata())->message);
}
}
9 changes: 9 additions & 0 deletions tests/NotImplementedTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,13 @@ public function testMethodNotImplemented(): void
$this->expectExceptionMessage('A grpc error with status code "UNIMPLEMENTED" and message "Unknown method Ping for service echos.api.v1.EchoService" occurred');
$client->invoke(new EchoRequest(), new Invoke('/echos.api.v1.EchoService/Ping', EchoResponse::class, RpcType::Unary));
}

public function testMethodNotImplementedMessageIsPercentEncoded(): void
{
$client = new Client\Builder()->build();

// The client URI layer encodes "%" in the path before it reaches the server, so the server reports the method as "100%25".
$this->expectExceptionMessage('A grpc error with status code "UNIMPLEMENTED" and message "Unknown method 100%25 for service echos.api.v1.EchoService" occurred');
$client->invoke(new EchoRequest(), new Invoke('/echos.api.v1.EchoService/100%', EchoResponse::class, RpcType::Unary));
}
}
26 changes: 26 additions & 0 deletions tests/UnaryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,28 @@ public function testServerHandlerException(): void

self::fail('Client::echo() above should be throw an exception');
}

public function testSpecialStatusMessage(): void
{
$client = new EchoServiceClient(
new Client\Builder()
->withUnaryInterceptors(new AuthorizationClientInterceptor('secret'))
->build(),
);

$message = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n";

try {
$client->echo(new EchoRequest($message), new Metadata()->with('server-status-message', '1'));
} catch (InvokeError $e) {
self::assertSame(Code::UNKNOWN, $e->statusCode);
self::assertSame($message, $e->statusMessage);

return;
}

self::fail('Client::echo() above should be throw an exception');
}
}

final readonly class UnaryEchoServer implements EchoServiceServer
Expand All @@ -138,6 +160,10 @@ public function echo(
]);
}

if ($md->value('server-status-message') === '1') {
throw new InvokeError(Code::UNKNOWN, $request->sentence);
}

$sentence = $md->value('server-sentence') ?? $request->sentence;

return new EchoResponse($sentence);
Expand Down
Loading