Spiral provides interceptors for gRPC services that allow you to intercept and modify requests and responses at various points in the request lifecycle.
See more
Read more about interceptors in the Framework — Interceptors section.
There are two types of interceptors:
Server interceptors are used to intercept and modify requests and responses received by a server. They are typically used to add cross-cutting functionality such as logging, authentication, or monitoring to the server.
Here is an example of a simple interceptor that logs a request before and after processing:
namespace App\Endpoint\GRPC\Interceptor;
use Psr\Log\LoggerInterface;
use Spiral\Interceptors\Context\CallContextInterface;
use Spiral\Interceptors\HandlerInterface;
use Spiral\Interceptors\InterceptorInterface;
final class LogInterceptor implements InterceptorInterface
{
public function __construct(
private readonly LoggerInterface $logger,
) {}
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed
{
$target = $context->getTarget();
$this->logger->info('Request received...', [
'target' => (string) $target,
'path' => $target->getPath(),
]);
$response = $handler->handle($context);
$this->logger->info('Request processed', [
'target' => (string)$target,
'path' => $target->getPath(),
]);
return $response;
}
}
Here is an example of a simple interceptor that handles exceptions thrown by the server. It will catch all exceptions and convert them to a gRPC exception.
namespace App\Endpoint\GRPC\Interceptor;
use Spiral\Exceptions\ExceptionReporterInterface;
use Spiral\Interceptors\Context\CallContextInterface;
use Spiral\Interceptors\HandlerInterface;
use Spiral\Interceptors\InterceptorInterface;
use Spiral\RoadRunner\GRPC\Exception\GRPCException;
use Spiral\RoadRunner\GRPC\Exception\GRPCExceptionInterface;
final class ExceptionHandlerInterceptor implements InterceptorInterface
{
public function __construct(
private readonly ExceptionReporterInterface $reporter,
) {}
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed
{
try {
return $handler->handle($context);
} catch (\Throwable $e) {
$this->reporter->report($e);
if ($e instanceof GRPCExceptionInterface) {
throw $e;
}
throw new GRPCException(
message: $e->getMessage(),
previous: $e,
);
}
}
}
Here is an example of a simple interceptor that receives trace context from the request.
namespace App\Endpoint\GRPC\Interceptor;
use Spiral\Interceptors\Context\CallContextInterface;
use Spiral\Interceptors\HandlerInterface;
use Spiral\Interceptors\InterceptorInterface;
use Spiral\Telemetry\TraceKind;
use Spiral\Telemetry\TracerFactoryInterface;
class InjectTelemetryFromContextInterceptor implements InterceptorInterface
{
public function __construct(
private readonly TracerFactoryInterface $tracerFactory,
) {}
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed
{
$ctx = $context->getArguments()[0] ?? null;
$traceContext = $ctx instanceof \Spiral\RoadRunner\GRPC\ContextInterface
? (array) $ctx->getValue('telemetry-trace-id')
: [];
$target = $context->getTarget();
return $this->tracerFactory->make($traceContext)->trace(
name: \sprintf('Interceptor [%s]', __CLASS__),
callback: static fn(): mixed => $handler->handle($context),
attributes: [
'target' => (string) $target,
'path' => $target->getPath(),
],
scoped: true,
traceKind: TraceKind::SERVER,
);
}
}
Here is an example of a simple interceptor that checks if the user is authenticated. It will use PHP attributes to determine which methods require authentication. An authentication token is passed in the request metadata.
namespace App\Endpoint\GRPC\Interceptor;
use App\Attribute\Guarded;
use Spiral\Attributes\ReaderInterface;
use Spiral\Interceptors\Context\CallContextInterface;
use Spiral\Interceptors\HandlerInterface;
use Spiral\Interceptors\InterceptorInterface;
use Spiral\RoadRunner\GRPC\ContextInterface;
final class GuardedInterceptor implements InterceptorInterface
{
public function __construct(
private readonly ReaderInterface $reader
) {
}
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed
{
$reflection = $context->getTarget()->getReflection();
if ($reflection !== null) {
$attribute = $this->reader->firstFunctionMetadata($reflection, Guarded::class);
if ($attribute !== null) {
$ctx = $context->getArguments()[0] ?? null;
if ($ctx instanceof ContextInterface) {
$this->checkAuth($attribute, $ctx);
}
}
}
return $handler->handle($context);
}
private function checkAuth(Guarded $attribute, ContextInterface $ctx): void
{
// Metadata always stores values as array.
$token = $ctx->getValue($attribute->tokenField)[0] ?? null;
// Here you can implement your own authentication logic
if ($token !== 'secret') {
throw new \Exception('Unauthorized.');
}
}
}
And example of a method that requires authentication:
use App\Attribute\Guarded;
#[Guarded]
public function ping(GRPC\ContextInterface $ctx, PingRequest $in): PingResponse
{
// ...
}
And example of Guarded attribute:
namespace App\Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor]
class Guarded
{
public function __construct(
public readonly string $tokenField = 'token'
) {
}
}
To use server interceptors, register them in the configuration file app/config/grpc.php
:
return [
'interceptors' => [
\App\Endpoint\GRPC\Interceptor\LogInterceptor::class,
\App\Endpoint\GRPC\Interceptor\ExceptionHandlerInterceptor::class,
\App\Endpoint\GRPC\Interceptor\GuardedInterceptor::class,
]
];
Client interceptors allow you to modify or extend the behavior of gRPC client requests. They can add functionality such as logging, timeout configuration, retries, and authentication.
See more
See the Client documentation for basic gRPC client configuration and usage.
Spiral provides several built-in client interceptors through the spiral/grpc-client
package:
Sets a timeout for each gRPC request in milliseconds:
use Spiral\Grpc\Client\Interceptor\SetTimoutInterceptor;
// Adds a 5-second timeout
SetTimoutInterceptor::createConfig(5_000)
Implements retry logic with exponential backoff for failed requests:
use Spiral\Grpc\Client\Interceptor\RetryInterceptor;
// Configure retries with custom parameters
RetryInterceptor::createConfig(
// Initial backoff interval in milliseconds (default: 50ms)
initialInterval: 50,
// Initial interval for resource exhaustion issues (default: 1000ms)
congestionInitialInterval: 1000,
// The coefficient for calculating next retry backoff (default: 2.0)
backoffCoefficient: 2.0,
// Maximum backoff interval (default: 100x of initialInterval)
maximumInterval: null,
// Maximum number of retry attempts (default: 0 means unlimited)
maximumAttempts: 3,
// Maximum jitter to apply to intervals (default: 0.1)
maximumJitterCoefficient: 0.1,
)
The RetryInterceptor
automatically retries requests that fail with one of these status codes:
ResourceExhausted
Unavailable
Unknown
Aborted
Rotates through multiple connections until the first successful response:
use Spiral\Grpc\Client\Interceptor\ConnectionsRotationInterceptor;
// Simply add the interceptor class
ConnectionsRotationInterceptor::class
This special interceptor calls service-specific interceptors defined in the service configuration.
If the ExecuteServiceInterceptors
interceptor is not included in the global interceptors list, service-specific interceptors will not be executed.
use Spiral\Grpc\Client\Interceptor\ExecuteServiceInterceptors;
// Required to execute service-specific interceptors
ExecuteServiceInterceptors::class
When writing custom client interceptors, you'll often need to access or modify elements of the gRPC request context.
The Spiral\Grpc\Client\Interceptor\Helper
class provides methods to safely work with these context fields:
use Spiral\Grpc\Client\Interceptor\Helper;
use Spiral\Interceptors\Context\CallContextInterface;
use Spiral\Interceptors\HandlerInterface;
use Spiral\Interceptors\InterceptorInterface;
final class AuthInterceptor implements InterceptorInterface
{
public function __construct(
private readonly string $authToken,
) {}
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed
{
// Get current metadata
$metadata = Helper::getMetadata($context);
// Add authentication token to metadata
$metadata['auth-token'] = [$this->authToken];
// Update context with new metadata
$context = Helper::withMetadata($context, $metadata);
// Continue the request pipeline
return $handler->handle($context);
}
}
The Helper
class provides the following methods:
getConnections()
/withConnections()
- Get/set available connectionsgetCurrentConnection()
/withCurrentConnection()
- Get/set current connectiongetMessage()
/withMessage()
- Get/set request messagegetMetadata()
/withMetadata()
- Get/set request metadatagetReturnType()
/withReturnType()
- Get/set expected response typegetOptions()
/withOptions()
- Get/set request optionsHere's an example of a custom client interceptor that injects telemetry data into the request:
use Spiral\Grpc\Client\Interceptor\Helper;
use Spiral\Interceptors\Context\CallContextInterface;
use Spiral\Interceptors\HandlerInterface;
use Spiral\Interceptors\InterceptorInterface;
use Spiral\Telemetry\TraceKind;
use Spiral\Telemetry\TracerInterface;
final class TelemetryInterceptor implements InterceptorInterface
{
public function __construct(
private readonly TracerInterface $tracer,
) {}
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed
{
// Get the current metadata
$metadata = Helper::getMetadata($context);
// Add trace context to metadata
$metadata['telemetry-trace-id'] = $this->tracer->getContext();
// Update context with new metadata
$context = Helper::withMetadata($context, $metadata);
// Trace the gRPC call
$target = $context->getTarget();
return $this->tracer->trace(
name: \sprintf('GRPC client request %s', (string) $target),
callback: static fn(): mixed => $handler->handle($context),
attributes: [
'target' => (string) $target,
'path' => $target->getPath(),
],
traceKind: TraceKind::PRODUCER,
);
}
}
The order of interceptors matters. Interceptors are executed in the order they are defined in the configuration. For example:
[
SetTimoutInterceptor::createConfig(10_000), // 10 second global timeout
RetryInterceptor::createConfig(maxAttempts: 3), // Up to 3 retries
SetTimoutInterceptor::createConfig(3_000), // 3 second per-attempt timeout
]
In this configuration:
The ExecuteServiceInterceptors
interceptor should typically be placed last in the global interceptors list to ensure that service-specific interceptors run after global ones.
For detailed information on configuring the gRPC client and its interceptors, see the Client documentation. Below is a basic example:
use App\GRPC\Interceptor\TelemetryInterceptor;
use Spiral\Grpc\Client\Config\GrpcClientConfig;
use Spiral\Grpc\Client\Config\ServiceConfig;
use Spiral\Grpc\Client\Config\ConnectionConfig;
use Spiral\Grpc\Client\Interceptor\SetTimoutInterceptor;
use Spiral\Grpc\Client\Interceptor\RetryInterceptor;
use Spiral\Grpc\Client\Interceptor\ExecuteServiceInterceptors;
return [
// ... server configuration ...
'client' => new GrpcClientConfig(
interceptors: [
// Global interceptors
TelemetryInterceptor::class,
SetTimoutInterceptor::createConfig(5_000),
RetryInterceptor::createConfig(maximumAttempts: 3),
// Calls service-specific interceptors
ExecuteServiceInterceptors::class,
],
services: [
new ServiceConfig(
connections: ConnectionConfig::createInsecure('localhost:9001'),
interfaces: [
\GRPC\MyService\ServiceInterface::class,
],
// Service-specific interceptors
interceptors: [
SetTimoutInterceptor::createConfig(2_000), // Override timeout for this service
],
),
],
)
];