From 2797cfcf026d3fac28637b3f771ff255be0926e5 Mon Sep 17 00:00:00 2001 From: Matthew Pearson Date: Sun, 2 Nov 2025 08:59:03 +0000 Subject: [PATCH] Copy the status details into the status runtime exception --- .../ClientStreamingServerCallHandler.java | 9 ++++-- .../grpc/internal/UnaryServerCallHandler.java | 30 +++++++++++++++++-- .../org/wiremock/grpc/GrpcAcceptanceTest.java | 18 +++++++++-- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/wiremock/grpc/internal/ClientStreamingServerCallHandler.java b/src/main/java/org/wiremock/grpc/internal/ClientStreamingServerCallHandler.java index c359ff2..eed83ae 100644 --- a/src/main/java/org/wiremock/grpc/internal/ClientStreamingServerCallHandler.java +++ b/src/main/java/org/wiremock/grpc/internal/ClientStreamingServerCallHandler.java @@ -17,6 +17,7 @@ import static org.wiremock.grpc.dsl.GrpcResponseDefinitionBuilder.GRPC_STATUS_NAME; import static org.wiremock.grpc.dsl.GrpcResponseDefinitionBuilder.GRPC_STATUS_REASON; +import static org.wiremock.grpc.internal.UnaryServerCallHandler.getTrailers; import com.github.tomakehurst.wiremock.common.Pair; import com.github.tomakehurst.wiremock.http.HttpHeader; @@ -24,6 +25,7 @@ import com.github.tomakehurst.wiremock.stubbing.ServeEvent; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; +import io.grpc.Metadata; import io.grpc.Status; import io.grpc.stub.ServerCalls; import io.grpc.stub.StreamObserver; @@ -49,6 +51,7 @@ public StreamObserver invoke(StreamObserver resp final AtomicReference firstResponse = new AtomicReference<>(); final AtomicReference responseStatus = new AtomicReference<>(); final AtomicReference statusReason = new AtomicReference<>(); + final AtomicReference trailers = new AtomicReference<>(); return new StreamObserver<>() { @Override @@ -91,6 +94,7 @@ public void onNext(DynamicMessage request) { responseStatus.set(status); statusReason.set(statusMapping.b); + trailers.set(getTrailers(resp)); return; } @@ -106,6 +110,7 @@ public void onNext(DynamicMessage request) { responseStatus.set(status); statusReason.set(reason); + trailers.set(getTrailers(resp)); return; } @@ -134,14 +139,14 @@ public void onCompleted() { responseObserver.onError( Status.fromCodeValue(responseStatus.get().getValue()) .withDescription(statusReason.get()) - .asRuntimeException()); + .asRuntimeException(trailers.get())); } else { final Pair notFoundStatusMapping = GrpcStatusUtils.errorHttpToGrpcStatusMappings.get(HttpStatus.NOT_FOUND_404); final Status grpcStatus = notFoundStatusMapping.a; responseObserver.onError( - grpcStatus.withDescription(notFoundStatusMapping.b).asRuntimeException()); + grpcStatus.withDescription(notFoundStatusMapping.b).asRuntimeException(trailers.get())); } } }; diff --git a/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java b/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java index bcf0175..5da7cb6 100644 --- a/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java +++ b/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java @@ -21,18 +21,28 @@ import com.github.tomakehurst.wiremock.common.Pair; import com.github.tomakehurst.wiremock.http.HttpHeader; +import com.github.tomakehurst.wiremock.http.Response; import com.github.tomakehurst.wiremock.http.StubRequestHandler; import com.github.tomakehurst.wiremock.stubbing.ServeEvent; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import io.grpc.Metadata; import io.grpc.Status; +import io.grpc.protobuf.ProtoUtils; import io.grpc.stub.ServerCalls; import io.grpc.stub.StreamObserver; import org.wiremock.grpc.dsl.WireMockGrpc; +import java.util.Base64; + public class UnaryServerCallHandler extends BaseCallHandler implements ServerCalls.UnaryMethod { + public static final Metadata.Key STATUS_DETAILS_KEY = + Metadata.Key.of("grpc-status-details-bin", ProtoUtils.metadataMarshaller( + com.google.rpc.Status.getDefaultInstance())); + public UnaryServerCallHandler( StubRequestHandler stubRequestHandler, Descriptors.ServiceDescriptor serviceDescriptor, @@ -69,7 +79,7 @@ public void invoke(DynamicMessage request, StreamObserver respon final Pair statusMapping = GrpcStatusUtils.errorHttpToGrpcStatusMappings.get(resp.getStatus()); responseObserver.onError( - statusMapping.a.withDescription(statusMapping.b).asRuntimeException()); + statusMapping.a.withDescription(statusMapping.b).asRuntimeException(getTrailers(resp))); return; } @@ -84,7 +94,7 @@ public void invoke(DynamicMessage request, StreamObserver respon responseObserver.onError( Status.fromCodeValue(status.getValue()) .withDescription(reason) - .asRuntimeException()); + .asRuntimeException(getTrailers(resp))); return; } @@ -98,4 +108,20 @@ public void invoke(DynamicMessage request, StreamObserver respon }, ServeEvent.of(wireMockRequest)); } + + public static Metadata getTrailers(Response resp) { + final HttpHeader statusDetailsHeader = resp.getHeaders().getHeader(STATUS_DETAILS_KEY.name()); + if (statusDetailsHeader.isPresent()) { + Metadata trailers = new Metadata(); + byte[] headerValue = Base64.getDecoder().decode(statusDetailsHeader.firstValue()); + try { + trailers.put(STATUS_DETAILS_KEY, com.google.rpc.Status.parseFrom(headerValue)); + return trailers; + } catch (InvalidProtocolBufferException e) { + // It would be nice if we could use the notifier to log this. + System.err.println("Failed to parse trailers from header. " + e.getMessage()); + } + } + return null; + } } diff --git a/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java b/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java index f30b1c0..6702dc6 100644 --- a/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java +++ b/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java @@ -40,6 +40,7 @@ import static org.wiremock.grpc.dsl.WireMockGrpc.message; import static org.wiremock.grpc.dsl.WireMockGrpc.messageAsAny; import static org.wiremock.grpc.dsl.WireMockGrpc.method; +import static org.wiremock.grpc.internal.UnaryServerCallHandler.STATUS_DETAILS_KEY; import com.example.grpc.AnotherGreetingServiceGrpc; import com.example.grpc.GreetingServiceGrpc; @@ -58,8 +59,11 @@ import io.grpc.reflection.v1.ServerReflectionResponse; import io.grpc.reflection.v1.ServiceResponse; import io.grpc.stub.StreamObserver; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.time.Duration; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.stream.Stream; @@ -205,14 +209,24 @@ void matchesRequestViaExactMessageEquality() { @ParameterizedTest @MethodSource("statusProvider") void shouldReturnTheCorrectGrpcErrorStatusForCorrespondingHttpStatus( - Integer httpStatus, String grpcStatus, String message) { + Integer httpStatus, String grpcStatus, String message) throws IOException { + com.google.rpc.Status gStatus = com.google.rpc.Status.newBuilder() + .setCode(io.grpc.Status.Code.valueOf(grpcStatus).value()) + .setMessage("Oh, dear! It was " + grpcStatus) + .build(); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + gStatus.writeTo(stream); wm.stubFor( post(urlPathEqualTo("/com.example.grpc.GreetingService/greeting")) - .willReturn(aResponse().withStatus(httpStatus))); + .willReturn(aResponse() + .withStatus(httpStatus) + .withHeader("grpc-status-details-bin", + Base64.getEncoder().encodeToString(stream.toByteArray())))); StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> greetingsClient.greet("Tom")); assertThat(exception.getMessage(), is(grpcStatus + ": " + message)); + assertThat(exception.getTrailers().get(STATUS_DETAILS_KEY).getMessage(), is("Oh, dear! It was " + grpcStatus)); } @Test