Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Java API for rich errors was not usable from Java #1937

Merged
merged 2 commits into from
Apr 22, 2024
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
2 changes: 1 addition & 1 deletion docs/src/main/paradox/client/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,6 @@ Scala
: @@snip [GreeterClient.scala](/interop-tests/src/test/scala/akka/grpc/scaladsl/RichErrorModelNativeSpec.scala) { #client_request }

Java
: @@snip[RichErrorModelSpec](/interop-tests/src/test/java/example/myapp/helloworld/grpc/RichErrorModelNativeTest.java) { #client_request }
: @@snip[RichErrorModelSpec](/plugin-tester-java/src/test/scala/example/myapp/helloworld/RichErrorModelNativeTest.java) { #client_request }

Please look @ref[here](../server/details.md) how to create errors with such details on the server side.
5 changes: 3 additions & 2 deletions docs/src/main/paradox/server/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ Java
## Rich error model
Beyond status codes you can also use the [Rich error model](https://www.grpc.io/docs/guides/error/#richer-error-model).

This example uses an error model taken from [common protobuf](https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto) but every class that is based on `scalapb.GeneratedMessage` can be used. Build and return the error as an `AkkaGrpcException`:
This example uses an error model taken from [common protobuf](https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto) but every class that is based on @scala[`scalapb.GeneratedMessage`]@java[`com.google.protobuf.Message`] can be used.
Build and return the error as an `AkkaGrpcException`:

Scala
: @@snip[RichErrorModelSpec](/interop-tests/src/test/scala/akka/grpc/scaladsl/RichErrorModelNativeSpec.scala) { #rich_error_model_unary }
johanandren marked this conversation as resolved.
Show resolved Hide resolved

Java
: @@snip[RichErrorModelTest](/interop-tests/src/test/java/example/myapp/helloworld/grpc/RichErrorNativeImpl.java) { #rich_error_model_unary }
: @@snip[RichErrorModelTest](/plugin-tester-java/src/test/scala/example/myapp/helloworld/RichErrorNativeImpl.java) { #rich_error_model_unary }

Please look @ref[here](../client/details.md) how to handle this on the client.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (C) 2018-2023 Lightbend Inc. <https://www.lightbend.com>
*/

package example.myapp.helloworld.grpc;
package example.myapp.helloworld;

import akka.actor.ActorSystem;
import akka.grpc.GrpcClientSettings;
Expand All @@ -12,9 +12,11 @@
import akka.http.javadsl.ServerBinding;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.HttpResponse;
import com.google.rpc.error_details.LocalizedMessage;
import akka.testkit.javadsl.TestKit;
import com.google.rpc.LocalizedMessage;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import example.myapp.helloworld.grpc.*;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.junit.Assert;
Expand Down Expand Up @@ -49,20 +51,20 @@ private ServerBinding run(ActorSystem sys) throws Exception {
public void testNativeApi() throws Exception {
Config conf = ConfigFactory.load();
ActorSystem sys = ActorSystem.create("HelloWorld", conf);
run(sys);

GrpcClientSettings settings = GrpcClientSettings.connectToServiceAt("127.0.0.1", 8091, sys).withTls(false);

GreeterServiceClient client = null;
try {
run(sys);

GrpcClientSettings settings = GrpcClientSettings.connectToServiceAt("127.0.0.1", 8091, sys).withTls(false);

client = GreeterServiceClient.create(settings, sys);

// #client_request
HelloRequest request = HelloRequest.newBuilder().setName("Alice").build();
CompletionStage<HelloReply> response = client.sayHello(request);
StatusRuntimeException statusRuntimeException = response.toCompletableFuture().handle((res, ex) -> {
return (StatusRuntimeException) ex;
}).get();
StatusRuntimeException statusRuntimeException = response.toCompletableFuture().handle((res, ex) ->
(StatusRuntimeException) ex
).get();

GrpcServiceException ex = GrpcServiceException.apply(statusRuntimeException);
MetadataStatus meta = (MetadataStatus) ex.getMetadata();
Expand All @@ -71,17 +73,15 @@ public void testNativeApi() throws Exception {
assertEquals(Status.INVALID_ARGUMENT.getCode().value(), meta.getCode());
assertEquals("What is wrong?", meta.getMessage());

LocalizedMessage details = meta.getParsedDetails(LocalizedMessage.messageCompanion()).get(0);
assertEquals("The password!", details.message());
assertEquals("EN", details.locale());
LocalizedMessage details = meta.getParsedDetails(LocalizedMessage.getDefaultInstance()).get(0);
Assert.assertEquals("The password!", details.getMessage());
Assert.assertEquals("EN", details.getLocale());
// #client_request

} catch (Exception e) {
e.printStackTrace();
Assert.fail("Got unexpected error " + e.getMessage());
} finally {
if (client != null) client.close();
sys.terminate();
if (client != null) {
client.close();
}
TestKit.shutdownActorSystem(sys);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
* Copyright (C) 2018-2023 Lightbend Inc. <https://www.lightbend.com>
*/

package example.myapp.helloworld.grpc;
package example.myapp.helloworld;

import akka.NotUsed;
import akka.grpc.GrpcServiceException;
import akka.stream.javadsl.Source;
import com.google.api.HttpBody;
import com.google.protobuf.Message;
import com.google.rpc.Code;
import com.google.rpc.error_details.LocalizedMessage;
import com.google.rpc.LocalizedMessage;
import example.myapp.helloworld.grpc.GreeterService;
import example.myapp.helloworld.grpc.HelloReply;
import example.myapp.helloworld.grpc.HelloRequest;

import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
Expand All @@ -20,8 +25,8 @@ public class RichErrorNativeImpl implements GreeterService {
@Override
public CompletionStage<HelloReply> sayHello(HelloRequest in) {

ArrayList<scalapb.GeneratedMessage> ar = new ArrayList<>();
ar.add(LocalizedMessage.of("EN", "The password!"));
ArrayList<Message> ar = new ArrayList<>();
ar.add(LocalizedMessage.newBuilder().setLocale("EN").setMessage("The password!").build());

GrpcServiceException exception = GrpcServiceException.create(
Code.INVALID_ARGUMENT,
Expand All @@ -35,6 +40,12 @@ public CompletionStage<HelloReply> sayHello(HelloRequest in) {
}
// #rich_error_model_unary


@Override
public CompletionStage<HttpBody> sayHelloHttp(HelloRequest in) {
throw new UnsupportedOperationException("Not implemented");
}

@Override
public CompletionStage<HelloReply> itKeepsTalking(Source<HelloRequest, NotUsed> in) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# This breaks compatibility but Java users could likely never use it anyway (scalpb message type as parameter)
ProblemFilters.exclude[IncompatibleSignatureProblem]("akka.grpc.GrpcServiceException.create")
# not for user extension
ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.grpc.javadsl.MetadataStatus.getParsedDetails")
33 changes: 15 additions & 18 deletions runtime/src/main/scala/akka/grpc/GrpcServiceException.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import akka.grpc.internal.{ JavaMetadataImpl, RichGrpcMetadataImpl }
import com.google.protobuf.any.Any
import io.grpc.protobuf.StatusProto

import java.util.{ List => JList }
import scala.jdk.CollectionConverters._

object GrpcServiceException {
Expand All @@ -21,22 +22,28 @@ object GrpcServiceException {
def create(
code: com.google.rpc.Code,
message: String,
details: java.util.List[scalapb.GeneratedMessage]): GrpcServiceException = {
apply(code, message, details.asScala.toVector)
details: JList[com.google.protobuf.Message]): GrpcServiceException = {
internalCreate(code, message, details.asScala.toVector.map(msg => com.google.protobuf.Any.pack(msg)))
}

/**
* Scala API
*/
def apply(
code: com.google.rpc.Code,
message: String,
details: Seq[scalapb.GeneratedMessage]): GrpcServiceException = {
def apply(code: com.google.rpc.Code, message: String, details: Seq[scalapb.GeneratedMessage]): GrpcServiceException =
internalCreate(code, message, details.map(msg => toJavaProto(Any.pack(msg))))

val status = com.google.rpc.Status.newBuilder().setCode(code.getNumber).setMessage(message)
def apply(ex: StatusRuntimeException): GrpcServiceException =
ex.getTrailers match {
case null =>
new GrpcServiceException(ex.getStatus)
case trailers =>
new GrpcServiceException(ex.getStatus, new RichGrpcMetadataImpl(ex.getStatus, trailers))
}

details.foreach(msg => status.addDetails(toJavaProto(Any.pack(msg))))
private def internalCreate(code: com.google.rpc.Code, message: String, details: Seq[com.google.protobuf.Any]) = {
val status = com.google.rpc.Status.newBuilder().setCode(code.getNumber).setMessage(message)

details.foreach(msg => status.addDetails(msg))
val statusRuntimeException = StatusProto.toStatusRuntimeException(status.build)

new GrpcServiceException(
Expand All @@ -50,16 +57,6 @@ object GrpcServiceException {
javaPbOut.setValue(scalaPbSource.value)
javaPbOut.build
}

def apply(ex: StatusRuntimeException): GrpcServiceException = {
ex.getTrailers match {
case null =>
new GrpcServiceException(ex.getStatus)
case trailers =>
new GrpcServiceException(ex.getStatus, new RichGrpcMetadataImpl(ex.getStatus, trailers))
}

}
}

@ApiMayChange
Expand Down
9 changes: 9 additions & 0 deletions runtime/src/main/scala/akka/grpc/internal/MetadataImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ class JavaMetadataImpl(val delegate: Metadata) extends javadsl.Metadata with jav

def getParsedDetails[K <: GeneratedMessage](companion: GeneratedMessageCompanion[K]): jList[K] =
richDelegate.getParsedDetails(companion).asJava

def getParsedDetails[K <: com.google.protobuf.GeneratedMessageV3](messageType: K): jList[K] = {
val parser = messageType.getParserForType
val messageTypeUrl = s"type.googleapis.com/${messageType.getDescriptorForType.getFullName}"
richDelegate.details.collect {
case scalaPbAny if scalaPbAny.typeUrl == messageTypeUrl =>
parser.parseFrom(scalaPbAny.value).asInstanceOf[K]
}.asJava
}
}

class RichGrpcMetadataImpl(delegate: io.grpc.Status, meta: io.grpc.Metadata)
Expand Down
13 changes: 8 additions & 5 deletions runtime/src/main/scala/akka/grpc/javadsl/Metadata.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

package akka.grpc.javadsl

import java.util.{ List, Map, Optional }
import java.util.{ List => JList, Map => JMap, Optional }
import akka.annotation.{ ApiMayChange, DoNotInherit }
import akka.util.ByteString
import akka.japi.Pair
Expand Down Expand Up @@ -35,13 +35,13 @@ trait Metadata {
* @return A map from keys to a list of metadata entries. Entries with the same key will be ordered based on
* when they were added or received.
*/
def asMap(): Map[String, List[MetadataEntry]]
def asMap(): JMap[String, JList[MetadataEntry]]

/**
* @return A list of (key, entry) pairs. Pairs with the same key will be ordered based on when they were added
* or received.
*/
def asList(): List[Pair[String, MetadataEntry]]
def asList(): JList[Pair[String, MetadataEntry]]

/**
* @return Returns the scaladsl.Metadata interface for this instance.
Expand All @@ -61,6 +61,9 @@ trait MetadataStatus extends Metadata {
def getStatus(): com.google.rpc.Status
def getCode(): Int
def getMessage(): String
def getDetails(): List[com.google.protobuf.any.Any]
def getParsedDetails[K <: scalapb.GeneratedMessage](companion: scalapb.GeneratedMessageCompanion[K]): List[K]
def getDetails(): JList[com.google.protobuf.any.Any]
@deprecated(message = "Use the new getParsedDetails overload taking a Java protobuf message type instead")
def getParsedDetails[K <: scalapb.GeneratedMessage](companion: scalapb.GeneratedMessageCompanion[K]): JList[K]

def getParsedDetails[K <: com.google.protobuf.GeneratedMessageV3](defaultMessage: K): JList[K]
}
Loading