diff --git a/vertx-grpc-client/src/main/java/io/vertx/grpc/client/InvalidStatusException.java b/vertx-grpc-client/src/main/java/io/vertx/grpc/client/InvalidStatusException.java index 6b76e8ac6..de41ab0cc 100644 --- a/vertx-grpc-client/src/main/java/io/vertx/grpc/client/InvalidStatusException.java +++ b/vertx-grpc-client/src/main/java/io/vertx/grpc/client/InvalidStatusException.java @@ -10,6 +10,7 @@ */ package io.vertx.grpc.client; +import io.vertx.core.MultiMap; import io.vertx.core.VertxException; import io.vertx.grpc.common.GrpcStatus; @@ -20,11 +21,13 @@ public final class InvalidStatusException extends VertxException { private final GrpcStatus expected; private final GrpcStatus actual; + private final MultiMap metadata; - public InvalidStatusException(GrpcStatus expected, GrpcStatus actual) { + public InvalidStatusException(GrpcStatus expected, GrpcStatus actual, MultiMap metadata) { super("Invalid status: actual:" + actual.name() + ", expected:" + expected.name()); this.expected = expected; this.actual = actual; + this.metadata = metadata; } /** @@ -40,4 +43,12 @@ public GrpcStatus expectedStatus() { public GrpcStatus actualStatus() { return actual; } + + /** + * @return the server trailers + */ + public MultiMap metadata() { + return metadata; + } + } diff --git a/vertx-grpc-client/src/main/java/io/vertx/grpc/client/impl/GrpcClientResponseImpl.java b/vertx-grpc-client/src/main/java/io/vertx/grpc/client/impl/GrpcClientResponseImpl.java index e47a2d2c4..933f6cd9c 100644 --- a/vertx-grpc-client/src/main/java/io/vertx/grpc/client/impl/GrpcClientResponseImpl.java +++ b/vertx-grpc-client/src/main/java/io/vertx/grpc/client/impl/GrpcClientResponseImpl.java @@ -112,7 +112,13 @@ public boolean test(Void value) { } @Override public Throwable describe(Void value) { - return new InvalidStatusException(GrpcStatus.OK, status()); + MultiMap metadata; + if (httpResponse.trailers().isEmpty()) { // TODO: Check if any payload has been parsed (needs GrpcReadStream modification) + metadata = httpResponse.headers(); // trailersOnly response + } else { + metadata = httpResponse.trailers(); + } + return new InvalidStatusException(GrpcStatus.OK, status(), metadata); } }); } diff --git a/vertx-grpc-client/src/test/java/io/vertx/tests/client/ClientRequestTest.java b/vertx-grpc-client/src/test/java/io/vertx/tests/client/ClientRequestTest.java index b5e41be3d..fed04c44b 100644 --- a/vertx-grpc-client/src/test/java/io/vertx/tests/client/ClientRequestTest.java +++ b/vertx-grpc-client/src/test/java/io/vertx/tests/client/ClientRequestTest.java @@ -142,6 +142,7 @@ public void testStatus(TestContext should) throws IOException { should.assertTrue(err instanceof InvalidStatusException); should.assertEquals(GrpcStatus.OK, ((InvalidStatusException)err).expectedStatus()); should.assertEquals(GrpcStatus.UNAVAILABLE, ((InvalidStatusException)err).actualStatus()); + should.assertEquals("error-value", ((InvalidStatusException)err).metadata().get("error-data")); latch2.complete(); })); })); diff --git a/vertx-grpc-client/src/test/java/io/vertx/tests/client/ClientTest.java b/vertx-grpc-client/src/test/java/io/vertx/tests/client/ClientTest.java index c49e76893..54d3c5399 100644 --- a/vertx-grpc-client/src/test/java/io/vertx/tests/client/ClientTest.java +++ b/vertx-grpc-client/src/test/java/io/vertx/tests/client/ClientTest.java @@ -291,8 +291,10 @@ public void testStatus(TestContext should) throws IOException { TestServiceGrpc.TestServiceImplBase called = new TestServiceGrpc.TestServiceImplBase() { @Override public void unary(Request request, StreamObserver responseObserver) { - responseObserver.onError(Status.UNAVAILABLE - .withDescription("~Greeter temporarily unavailable...~").asRuntimeException()); + Metadata metadata = new Metadata(); + metadata.put(Metadata.Key.of("error-data", Metadata.ASCII_STRING_MARSHALLER), "error-value"); + var re = Status.UNAVAILABLE.withDescription("~Greeter temporarily unavailable...~").asRuntimeException(metadata); + responseObserver.onError(re); } }; startServer(called); diff --git a/vertx-grpc-docs/src/main/java/examples/grpc/GreeterGrpcClient.java b/vertx-grpc-docs/src/main/java/examples/grpc/GreeterGrpcClient.java index fa1469cf5..d4631ff0a 100644 --- a/vertx-grpc-docs/src/main/java/examples/grpc/GreeterGrpcClient.java +++ b/vertx-grpc-docs/src/main/java/examples/grpc/GreeterGrpcClient.java @@ -3,6 +3,7 @@ import io.vertx.core.Future; import io.vertx.core.Completable; import io.vertx.core.Handler; +import io.vertx.core.MultiMap; import io.vertx.core.net.SocketAddress; import io.vertx.grpc.client.GrpcClient; import io.vertx.core.streams.ReadStream; diff --git a/vertx-grpc-docs/src/main/java/examples/grpc/StreamingGrpcClient.java b/vertx-grpc-docs/src/main/java/examples/grpc/StreamingGrpcClient.java index a0cb88f6c..40506e2c9 100644 --- a/vertx-grpc-docs/src/main/java/examples/grpc/StreamingGrpcClient.java +++ b/vertx-grpc-docs/src/main/java/examples/grpc/StreamingGrpcClient.java @@ -3,6 +3,7 @@ import io.vertx.core.Future; import io.vertx.core.Completable; import io.vertx.core.Handler; +import io.vertx.core.MultiMap; import io.vertx.core.net.SocketAddress; import io.vertx.grpc.client.GrpcClient; import io.vertx.core.streams.ReadStream; @@ -93,7 +94,13 @@ public Future> source(examples.grpc.Empty request req.format(wireFormat); return req.end(request).compose(v -> req.response().flatMap(resp -> { if (resp.status() != null && resp.status() != GrpcStatus.OK) { - return Future.failedFuture(new io.vertx.grpc.client.InvalidStatusException(GrpcStatus.OK, resp.status())); + MultiMap metadata; + if (resp.trailers().isEmpty()) { // TODO: Check if any payload has been parsed (needs GrpcReadStream modification) + metadata = resp.headers(); // trailersOnly response + } else { + metadata = resp.trailers(); + } + return Future.failedFuture(new io.vertx.grpc.client.InvalidStatusException(GrpcStatus.OK, resp.status(), metadata)); } else { return Future.succeededFuture(resp); } @@ -125,7 +132,13 @@ public Future> pipe(Completable { return req.response().flatMap(resp -> { if (resp.status() != null && resp.status() != GrpcStatus.OK) { - return Future.failedFuture(new io.vertx.grpc.client.InvalidStatusException(GrpcStatus.OK, resp.status())); + MultiMap metadata; + if (resp.trailers().isEmpty()) { // TODO: Check if any payload has been parsed (needs GrpcReadStream modification) + metadata = resp.headers(); // trailersOnly response + } else { + metadata = resp.trailers(); + } + return Future.failedFuture(new io.vertx.grpc.client.InvalidStatusException(GrpcStatus.OK, resp.status(), metadata)); } else { return Future.succeededFuture(resp); } diff --git a/vertx-grpc-it/src/test/java/io/vertx/grpc/it/ProtocPluginTestBase.java b/vertx-grpc-it/src/test/java/io/vertx/grpc/it/ProtocPluginTestBase.java index 3331e495b..3999e8bd5 100644 --- a/vertx-grpc-it/src/test/java/io/vertx/grpc/it/ProtocPluginTestBase.java +++ b/vertx-grpc-it/src/test/java/io/vertx/grpc/it/ProtocPluginTestBase.java @@ -11,12 +11,14 @@ package io.vertx.grpc.it; import com.google.protobuf.ByteString; +import io.grpc.Metadata; import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.examples.helloworld.*; import io.grpc.testing.integration.*; import io.vertx.core.Completable; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import io.vertx.core.Promise; import io.vertx.core.http.HttpServer; import io.vertx.core.net.SocketAddress; @@ -27,7 +29,9 @@ import io.vertx.grpc.client.GrpcClient; import io.vertx.grpc.client.GrpcClientRequest; import io.vertx.grpc.client.InvalidStatusException; +import io.vertx.grpc.common.GrpcHeaderNames; import io.vertx.grpc.common.GrpcStatus; +import io.vertx.grpc.common.impl.Utils; import io.vertx.grpc.server.StatusException; import io.vertx.grpc.server.GrpcServer; import io.vertx.grpc.server.Service; @@ -740,6 +744,18 @@ public Future sayHello(HelloRequest request) { }); } + @Test + public void testServerStatus3(TestContext should) throws Exception { + testServerStatus(should, new GreeterService() { + @Override + public Future sayHello(HelloRequest request) { + MultiMap errorContext = MultiMap.caseInsensitiveMultiMap(); + errorContext.add("error-info", "error-data"); + return Future.failedFuture(new StatusException(GrpcStatus.INTERNAL, "error message", errorContext)); + } + }); + } + private void testServerStatus(TestContext should, GreeterService service) throws Exception { // Create gRPC Server @@ -760,11 +776,25 @@ private void testServerStatus(TestContext should, GreeterService service) throws .onComplete(should.asyncAssertFailure(reply -> { if (reply instanceof InvalidStatusException) { InvalidStatusException ise = (InvalidStatusException) reply; - should.assertEquals(GrpcStatus.NOT_FOUND, ise.actualStatus()); + MultiMap metadata = ise.metadata(); + if (!metadata.contains("error-info")) { // testServerStatus1, testServerStatus2 + should.assertEquals(GrpcStatus.NOT_FOUND, ise.actualStatus()); + } else { // testServerStatus3 + should.assertEquals(GrpcStatus.INTERNAL, ise.actualStatus()); + should.assertEquals( "error-data", metadata.get("error-info")); + should.assertEquals(Utils.utf8PercentEncode("error message"), metadata.get(GrpcHeaderNames.GRPC_MESSAGE)); + } test.complete(); } else if (reply instanceof StatusRuntimeException) { StatusRuntimeException sre = (StatusRuntimeException) reply; - should.assertEquals(Status.NOT_FOUND, sre.getStatus()); + Metadata metadata = sre.getTrailers(); + Metadata.Key key = Metadata.Key.of("error-info", Metadata.ASCII_STRING_MARSHALLER); + if (!metadata.containsKey(key)) { // testServerStatus1, testServerStatus2 + should.assertEquals(Status.NOT_FOUND, sre.getStatus()); + } else { // testServerStatus3 + should.assertEquals(Status.INTERNAL, sre.getStatus()); + should.assertEquals( "error-data", metadata.get(key)); + } test.complete(); } else { should.fail(); diff --git a/vertx-grpc-protoc-plugin2/src/main/resources/grpc-client.mustache b/vertx-grpc-protoc-plugin2/src/main/resources/grpc-client.mustache index d27719845..0f2325523 100644 --- a/vertx-grpc-protoc-plugin2/src/main/resources/grpc-client.mustache +++ b/vertx-grpc-protoc-plugin2/src/main/resources/grpc-client.mustache @@ -5,6 +5,7 @@ package {{javaPackageFqn}}; import io.vertx.core.Future; import io.vertx.core.Completable; import io.vertx.core.Handler; +import io.vertx.core.MultiMap; import io.vertx.core.net.SocketAddress; import io.vertx.grpc.client.GrpcClient; import io.vertx.core.streams.ReadStream; @@ -95,7 +96,13 @@ class {{grpcClientFqn}}Impl implements {{grpcClientFqn}} { req.format(wireFormat); return req.end(request).compose(v -> req.response().flatMap(resp -> { if (resp.status() != null && resp.status() != GrpcStatus.OK) { - return Future.failedFuture(new io.vertx.grpc.client.InvalidStatusException(GrpcStatus.OK, resp.status())); + MultiMap metadata; + if (resp.trailers().isEmpty()) { // TODO: Check if any payload has been parsed (needs GrpcReadStream modification) + metadata = resp.headers(); // trailersOnly response + } else { + metadata = resp.trailers(); + } + return Future.failedFuture(new io.vertx.grpc.client.InvalidStatusException(GrpcStatus.OK, resp.status(), metadata)); } else { return Future.succeededFuture(resp); } @@ -131,7 +138,13 @@ class {{grpcClientFqn}}Impl implements {{grpcClientFqn}} { .compose(req -> { return req.response().flatMap(resp -> { if (resp.status() != null && resp.status() != GrpcStatus.OK) { - return Future.failedFuture(new io.vertx.grpc.client.InvalidStatusException(GrpcStatus.OK, resp.status())); + MultiMap metadata; + if (resp.trailers().isEmpty()) { + metadata = resp.headers(); // trailersOnly response + } else { + metadata = resp.trailers(); + } + return Future.failedFuture(new io.vertx.grpc.client.InvalidStatusException(GrpcStatus.OK, resp.status(), metadata)); } else { return Future.succeededFuture(resp); } diff --git a/vertx-grpc-server/src/main/java/io/vertx/grpc/server/GrpcErrorInfoProvider.java b/vertx-grpc-server/src/main/java/io/vertx/grpc/server/GrpcErrorInfoProvider.java new file mode 100644 index 000000000..a34faa845 --- /dev/null +++ b/vertx-grpc-server/src/main/java/io/vertx/grpc/server/GrpcErrorInfoProvider.java @@ -0,0 +1,41 @@ +package io.vertx.grpc.server; + +import io.vertx.core.MultiMap; +import io.vertx.grpc.common.GrpcStatus; + +/** + * Interface for providing detailed error information in gRPC server responses. + *

+ * Implementing this interface allows exceptions to expose structured gRPC error details, + * including a status a descriptive error message, and optional trailers. + *

+ *

+ * This design enables custom exceptions to propagate meaningful and rich error context to gRPC clients + * without coupling to a specific exception class. + *

+ */ + +public interface GrpcErrorInfoProvider { + + /** + * Returns the GrpcStatus associated with this error. + * + * @return the gRPC status + */ + GrpcStatus status(); + + /** + * Returns the gRPC error message to send to the client. + * + * @return the error message as a string + */ + String message(); + + /** + * Returns optional key-value trailers to include in the response. + * Can be {@code null} or empty. + * + * @return containing error trailers + */ + MultiMap trailers(); +} diff --git a/vertx-grpc-server/src/main/java/io/vertx/grpc/server/GrpcServerResponse.java b/vertx-grpc-server/src/main/java/io/vertx/grpc/server/GrpcServerResponse.java index 364d2a43c..edea6c3e3 100644 --- a/vertx-grpc-server/src/main/java/io/vertx/grpc/server/GrpcServerResponse.java +++ b/vertx-grpc-server/src/main/java/io/vertx/grpc/server/GrpcServerResponse.java @@ -81,7 +81,8 @@ default Future send(ReadStream body) { * End the stream with an appropriate status message, when {@code failure} is * *
    - *
  • {@link StatusException}, set status to {@link StatusException#status()} and status message to {@link StatusException#message()}
  • + *
  • {@link StatusException}, set status to {@link StatusException#status()}, status message to {@link StatusException#message()} and associated metadata to {@link StatusException#trailers()}
  • + *
  • Use any exception implementing {@link GrpcErrorInfoProvider} to propagate meaningful and rich error context to gRPC clients without coupling to a specific exception class.
  • *
  • {@link UnsupportedOperationException} returns {@link GrpcStatus#UNIMPLEMENTED}
  • *
  • otherwise returns {@link GrpcStatus#UNKNOWN}
  • *
diff --git a/vertx-grpc-server/src/main/java/io/vertx/grpc/server/StatusException.java b/vertx-grpc-server/src/main/java/io/vertx/grpc/server/StatusException.java index 45739b1bf..76547b4fc 100644 --- a/vertx-grpc-server/src/main/java/io/vertx/grpc/server/StatusException.java +++ b/vertx-grpc-server/src/main/java/io/vertx/grpc/server/StatusException.java @@ -10,40 +10,46 @@ */ package io.vertx.grpc.server; +import io.vertx.core.MultiMap; import io.vertx.core.VertxException; import io.vertx.grpc.common.GrpcStatus; /** * A glorified GOTO forcing a response status. */ -public final class StatusException extends VertxException { +public final class StatusException extends VertxException implements GrpcErrorInfoProvider { private final GrpcStatus status; private final String message; + private final MultiMap trailers; public StatusException(GrpcStatus status) { - super("Grpc status " + status.name()); - this.status = status; - this.message = null; + this(status, null, null); } public StatusException(GrpcStatus status, String message) { + this(status, message, null); + } + + public StatusException(GrpcStatus status, String message, MultiMap trailers) { super("Grpc status " + status.name()); this.status = status; this.message = message; + this.trailers = trailers; } - /** - * @return the status - */ + @Override public GrpcStatus status() { return status; } - /** - * @return the status message - */ + @Override public String message() { return message; } + + @Override + public MultiMap trailers() { + return trailers; + } } diff --git a/vertx-grpc-server/src/main/java/io/vertx/grpc/server/impl/GrpcServerResponseImpl.java b/vertx-grpc-server/src/main/java/io/vertx/grpc/server/impl/GrpcServerResponseImpl.java index af65296f7..322ce5312 100644 --- a/vertx-grpc-server/src/main/java/io/vertx/grpc/server/impl/GrpcServerResponseImpl.java +++ b/vertx-grpc-server/src/main/java/io/vertx/grpc/server/impl/GrpcServerResponseImpl.java @@ -22,10 +22,9 @@ import io.vertx.grpc.common.impl.GrpcMessageImpl; import io.vertx.grpc.common.impl.GrpcWriteStreamBase; import io.vertx.grpc.common.impl.Utils; +import io.vertx.grpc.server.GrpcErrorInfoProvider; import io.vertx.grpc.server.GrpcProtocol; import io.vertx.grpc.server.GrpcServerResponse; -import io.vertx.grpc.server.StatusException; - import java.util.Map; import java.util.Objects; @@ -79,10 +78,17 @@ public void handleTimeout() { } public void fail(Throwable failure) { - if (failure instanceof StatusException) { - StatusException se = (StatusException) failure; - this.status = se.status(); - this.statusMessage = se.message(); + if (failure instanceof GrpcErrorInfoProvider) { + GrpcErrorInfoProvider infoPro = (GrpcErrorInfoProvider) failure; + this.status = infoPro.status(); + this.statusMessage = infoPro.message(); + MultiMap errorTrailers = infoPro.trailers(); + if (errorTrailers != null && !errorTrailers.isEmpty()) { + MultiMap grpcTrailers = trailers(); + for (Map.Entry header : errorTrailers) { + grpcTrailers.add(header.getKey(), header.getValue()); + } + } } else { this.status = mapStatus(failure); } @@ -185,8 +191,8 @@ protected Buffer encodeMessage(Buffer message, boolean compressed, boolean trail } private static GrpcStatus mapStatus(Throwable t) { - if (t instanceof StatusException) { - return ((StatusException)t).status(); + if (t instanceof GrpcErrorInfoProvider) { + return ((GrpcErrorInfoProvider) t).status(); } else if (t instanceof UnsupportedOperationException) { return GrpcStatus.UNIMPLEMENTED; } else { diff --git a/vertx-grpc-server/src/test/java/io/vertx/tests/server/ServerRequestTest.java b/vertx-grpc-server/src/test/java/io/vertx/tests/server/ServerRequestTest.java index f0575e09d..eea5b7acd 100644 --- a/vertx-grpc-server/src/test/java/io/vertx/tests/server/ServerRequestTest.java +++ b/vertx-grpc-server/src/test/java/io/vertx/tests/server/ServerRequestTest.java @@ -143,6 +143,21 @@ public void testStatusUnary4(TestContext should) { super.testStatusUnary(should, Status.ALREADY_EXISTS, "status-msg"); } + @Test + public void testStatusUnary5(TestContext should) { + + MultiMap trailers = MultiMap.caseInsensitiveMultiMap(); + trailers.add("error-data", "error-value"); + + startServer(GrpcServer.server(vertx).callHandler(UNARY, call -> { + call.handler(helloRequest -> { + throw new StatusException(GrpcStatus.ALREADY_EXISTS, "status-msg", trailers); + }); + })); + + super.testStatusUnary(should, Status.ALREADY_EXISTS, "status-msg", trailers); + } + @Test public void testStatusStreaming(TestContext should) { startServer(GrpcServer.server(vertx).callHandler(SOURCE, call -> { diff --git a/vertx-grpc-server/src/test/java/io/vertx/tests/server/ServerTest.java b/vertx-grpc-server/src/test/java/io/vertx/tests/server/ServerTest.java index b01a6a147..7a704c381 100644 --- a/vertx-grpc-server/src/test/java/io/vertx/tests/server/ServerTest.java +++ b/vertx-grpc-server/src/test/java/io/vertx/tests/server/ServerTest.java @@ -100,8 +100,12 @@ public void onHeaders(Metadata headers) { } public void testStatusUnary(TestContext should, Status expectedStatus, String expectedStatusMessage) { + testStatusUnary(should, expectedStatus, expectedStatusMessage, null); + } + + public void testStatusUnary(TestContext should, Status expectedStatus, String expectedStatusMessage, MultiMap expectedTrailers) { Request request = Request.newBuilder().setName("Julien").build(); - channel = ManagedChannelBuilder.forAddress( "localhost", port) + channel = ManagedChannelBuilder.forAddress("localhost", port) .usePlaintext() .build(); TestServiceGrpc.TestServiceBlockingStub stub = TestServiceGrpc.newBlockingStub(channel); @@ -111,6 +115,15 @@ public void testStatusUnary(TestContext should, Status expectedStatus, String ex } catch (StatusRuntimeException e) { should.assertEquals(expectedStatus.getCode(), e.getStatus().getCode()); should.assertEquals(expectedStatusMessage, e.getStatus().getDescription()); + if (expectedTrailers != null) { + Metadata trailers = e.getTrailers(); + should.assertNotNull(trailers, "No trailers provided in response"); + for (Map.Entry expectedTrailer : expectedTrailers.entries()) { + Metadata.Key key = Metadata.Key.of(expectedTrailer.getKey(), Metadata.ASCII_STRING_MARSHALLER); + should.assertEquals(expectedTrailer.getValue(), trailers.get(key)); + } + } + } } diff --git a/vertx-grpcio-client/src/main/java/io/vertx/grpcio/client/VertxClientCall.java b/vertx-grpcio-client/src/main/java/io/vertx/grpcio/client/VertxClientCall.java index 8a753110a..401895519 100644 --- a/vertx-grpcio-client/src/main/java/io/vertx/grpcio/client/VertxClientCall.java +++ b/vertx-grpcio-client/src/main/java/io/vertx/grpcio/client/VertxClientCall.java @@ -9,6 +9,7 @@ import io.grpc.Metadata; import io.grpc.Status; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import io.vertx.core.net.SocketAddress; import io.vertx.grpc.client.GrpcClientRequest; import io.vertx.grpc.client.GrpcClientResponse; @@ -130,7 +131,14 @@ public void start(Listener responseListener, Metadata headers) { if (grpcResponse.statusMessage() != null) { status = status.withDescription(grpcResponse.statusMessage()); } - trailers = io.vertx.grpcio.common.impl.Utils.readMetadata(grpcResponse.trailers()); + MultiMap responseTrailers; + boolean trailersOnly = grpcResponse.trailers().isEmpty(); + if (trailersOnly) { + responseTrailers = grpcResponse.headers(); + } else { + responseTrailers = grpcResponse.trailers(); + } + trailers = io.vertx.grpcio.common.impl.Utils.readMetadata(responseTrailers); } else { status = Status.fromThrowable(ar.cause()); trailers = new Metadata(); diff --git a/vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/impl/stub/ServerCalls.java b/vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/impl/stub/ServerCalls.java index 6433e8e7d..59a057b37 100644 --- a/vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/impl/stub/ServerCalls.java +++ b/vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/impl/stub/ServerCalls.java @@ -15,16 +15,19 @@ */ package io.vertx.grpcio.server.impl.stub; +import io.grpc.Metadata; import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.stub.CallStreamObserver; import io.grpc.stub.ServerCallStreamObserver; import io.grpc.stub.StreamObserver; import io.vertx.core.Completable; +import io.vertx.core.MultiMap; import io.vertx.core.internal.ContextInternal; import io.vertx.core.streams.ReadStream; import io.vertx.core.streams.WriteStream; import io.vertx.grpc.server.StatusException; +import io.vertx.grpcio.common.impl.Utils; import io.vertx.grpcio.common.impl.stub.GrpcWriteStream; import io.vertx.grpcio.common.impl.stub.StreamObserverReadStream; @@ -69,7 +72,7 @@ public static StreamObserver manyToOne(ContextInternal ctx, StreamObse trySetCompression(response, compression); StreamObserverReadStream request = new StreamObserverReadStream<>(ctx, (CallStreamObserver) response); request.init(); - Completable completable = (res,err) -> { + Completable completable = (res, err) -> { if (err == null) { response.onNext(res); response.onCompleted(); @@ -109,7 +112,14 @@ private static void trySetCompression(StreamObserver response, String compres private static Throwable prepareError(Throwable throwable) { if (throwable instanceof StatusException) { - return new StatusRuntimeException(Status.fromCode(Status.Code.valueOf(((StatusException)throwable).status().name()))); + StatusException se = (StatusException) throwable; + Status status = Status.fromCode(Status.Code.valueOf(se.status().name())); + Metadata metadata = null; + MultiMap trailers = se.trailers(); + if (trailers != null) { + metadata = Utils.readMetadata(trailers); + } + return new StatusRuntimeException(status, metadata); } else if (throwable instanceof UnsupportedOperationException) { return new StatusRuntimeException(Status.UNIMPLEMENTED); } else if (throwable instanceof io.grpc.StatusException || throwable instanceof StatusRuntimeException) {