Skip to content

Conversation

@adparts
Copy link

@adparts adparts commented Jul 18, 2025

This PR introduces support for propagating structured gRPC error metadata from the server to the client across the Vert.x gRPC stack. It allows both client and server components to handle and transmit metadata in error responses consistently.

Motivation:
Handling rich error information in gRPC is critical for debugging, client behavior, and user feedback. Until now, only status codes and messages were propagated, with no support for custom trailers or metadata.

This change allows:

  • Server-side exceptions to carry metadata.
  • Middleware and clients to access that metadata.
  • End-to-end visibility of error context for both internal and external clients.

This feature is required for internal use at our company, where we rely on Vert.x-based gRPC services and need to provide detailed error context (status, message, and metadata) from server to client. Without this functionality, we are unable to fully adopt vertx-grpc in our projects.

Changes

Server
Created GrpcErrorInfoProvider interface to allow exceptions to expose:

  • A GrpcStatus
  • A message
  • Metadata (io.vertx.core.MultiMap)

Made existing StatusException implement GrpcErrorInfoProvider and adapt GrpcServerResponseImpl to return the metadata from GrpcErrorInfoProvider

Client
Updated InvalidStatusException to include a metadata field.
Modified GrpcClientResponseImpl and the gRPC proto plugin to populate the metadata field when the returned status is not GrpcStatus.OK.

Vert.x gRPC-IO Layer
Server: Inject metadata into StatusRuntimeException for correct transmission to clients.
Client: Parse metadata from headers if the response is trailersOnly, not just from trailers.

Improved (but still not ideal) detection of trailersOnly responses by checking if a payload has been read.

adparts added 3 commits July 18, 2025 15:34
…dling

Grpc Client:
- Updated InvalidStatusException to include a metadata field for error context.
- GrpcClientResponseImpl now sets the metadata field when the returned status is not GrpcStatus.OK.

gRPC Proto Plugin:
- Populates the metadata field on non-OK responses, enabling clients to access additional error details.

Vert.x gRPC-IO Client:
- Parses metadata from response headers when the response is trailersOnly.
  (Previously, metadata was only parsed from trailers.)

Note:
- trailersOnly detection works but remains somewhat hacky; it could be improved by explicitly tracking whether any payload has been read.
Vert.x gRPC Server:
- Introduced the GrpcErrorInfoProvider interface to allow server-side exceptions   to expose gRPC error details, including custom metadata.
- Made StatusException implement GrpcErrorInfoProvider, enabling richer error context.
- Updated GrpcServerResponseImpl to detect exceptions implementing GrpcErrorInfoProvider and include their metadata in the gRPC error response sent to the client.

Vert.x gRPC-IO Server:
- Modified server logic to attach received metadata to the generated StatusRuntimeException, preserving trailer information in error propagation.
vertx-grpc-client-test:
- Added tests to verify that error responses include and correctly expose metadata.

vertx-grpc-server-test:
- Added tests to ensure that server-side exceptions with metadata are correctly sent to clients.

vertx-grpc-it-test:
- Implemented integration tests covering end-to-end error propagation,   including custom metadata in gRPC error responses.
@vietj vietj added this to the 5.1.0 milestone Jul 22, 2025
* </p>
*/

public interface GrpcErrorInfoProvider {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this interface is needed for implementing the feature ? if we can just use directly StatusException without it, then we should do it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right it's not necessary, but there’s an important reason behind it:

StatusException is final, applications cannot extend it to include additional behavior or metadata relevant to their domain logic. As a result, if we rely solely on StatusException, applications are forced to throw a specific Vert.x class from within their business logic, making the error-handling tightly coupled to the transport layer.

The goal of introducing the interface is to decouple the business logic from the transport mechanism. This way, applications can throw their own domain-specific exceptions (e.g. InvalidUserInputException, PaymentRejectedException, etc.) and simply implement the interface to expose gRPC-compatible error data (status, message, metadata).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally like the idea of users having the ability to write their own exceptions which are decoupled from the transport layer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently there are not tests using GrpcErrorInfoProvider, can you explain how this would be used in practice, it is not yet still clear to me how it can be used

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review. Let me give a concrete example to clarify the motivation for the interface.

Imagine a typical application structure where we define a base exception for our domain logic, and specific exceptions extend from it. These exceptions should not extend from StatusException, which is Vert.x/gRPC specific and has nothing to do with our application's business concerns (Furthermore, StatusException is final and we force the application to use it without chance of any subclassing).

Example

// Our base exception for the application
public abstract class AppException extends RuntimeException {
  public AppException(String message) {
    super(message);
  }
}

Then we define a business-specific exception, and we want it to be mappable to a gRPC status, without coupling it to Vert.x types:

// A business logic exception that we want to map to gRPC
public class UserNotFoundException extends AppException implements GrpcStatusException {
  public UserNotFoundException(String userId) {
    super("User not found: " + userId);
  }

  @Override
  public Status getGrpcStatus() {
    return Status.NOT_FOUND.withDescription(getMessage());
  }

  // add metadata
  // add needed logic
}

Note that:

  • UserNotFoundException is part of our domain, and extends AppException.
  • It implements GrpcStatusException to signal to the framework that it knows how to convert itself to a gRPC Status.

This keeps our application cleanly decoupled, while still enabling powerful mapping logic on the framework side.

Without this interface, we are forced to use StatusException directly within our application code.

  1. StatusException is a final class, meaning we cannot extend it to add application-specific data or behavior that might be relevant (e.g. error codes, metadata, logging context, etc.).
  2. It couples our domain logic to Vert.x internals, which breaks separation of concerns.

This pattern is especially useful in microservice-to-microservice calls, where domain exceptions need to be translated to appropriate gRPC statuses but we still want to keep a clean architecture on the application side.

Hope this clarifies the intent!

@vietj
Copy link
Member

vietj commented Sep 8, 2025

@adparts we would like actually to use an exception mapper strategy to transform an exception into a StatusException, so we make it less intrusive, this mapper would be set for now on the ServiceBuilder as a Function<Throwable, StatusException> and used when deploying a service

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants