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

Rich error model client api #1665

Merged
merged 2 commits into from
Dec 21, 2022
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
8 changes: 4 additions & 4 deletions docs/src/main/paradox/client/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@ 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). Currently there is no particular support for consuming such error objects (such as the ones based on the [common protobuf](https://cloud.google.com/apis/design/errors#error_model), but you can obtain them 'manually'.
Beyond status codes you can also use the [Rich error model](https://www.grpc.io/docs/guides/error/#richer-error-model).

Extract the `StatusRuntimeException` and parse the Rich error model to access `code`, `message` and `details`. Then find the details you are looking for based on their `typeUrl` and unpack them:
Extract the `GrpcServiceException` to access `code`, `message` and `details`.

Scala
: @@snip [GreeterClient.scala](/interop-tests/src/test/scala/akka/grpc/scaladsl/RichErrorModelSpec.scala) { #client_request }
: @@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/RichErrorModelTest.java) { #client_request }
: @@snip[RichErrorModelSpec](/interop-tests/src/test/java/example/myapp/helloworld/grpc/RichErrorModelNativeTest.java) { #client_request }

Please look @ref[here](../server/details.md) how to create errors with such details on the server side.
4 changes: 2 additions & 2 deletions docs/src/main/paradox/server/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ Beyond status codes you can also use the [Rich error model](https://www.grpc.io/
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`:

Scala
: @@snip[RichErrorModelSpec](/interop-tests/src/test/scala/akka/grpc/scaladsl/RichErrorModelSpec.scala) { #native_rich_error_model_unary }
: @@snip[RichErrorModelSpec](/interop-tests/src/test/scala/akka/grpc/scaladsl/RichErrorModelNativeSpec.scala) { #rich_error_model_unary }

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

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

package example.myapp.helloworld.grpc;

import akka.actor.ActorSystem;
import akka.grpc.GrpcClientSettings;
import akka.grpc.GrpcServiceException;
import akka.grpc.internal.JavaMetadataImpl;
import akka.grpc.internal.RichGrpcMetadataImpl;
import akka.grpc.javadsl.RichMetadata;
import akka.http.javadsl.Http;
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 com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.junit.Assert;
import org.junit.Test;
import org.scalatestplus.junit.JUnitSuite;

import java.util.concurrent.CompletionStage;

import static org.junit.Assert.assertEquals;


public class RichErrorModelNativeTest extends JUnitSuite {

private ServerBinding run(ActorSystem sys) throws Exception {

GreeterService impl = new RichErrorNativeImpl();

akka.japi.function.Function<HttpRequest, CompletionStage<HttpResponse>> service = GreeterServiceHandlerFactory.create(impl, sys);
CompletionStage<ServerBinding> bound = Http
.get(sys)
.newServerAt("127.0.0.1", 8091)
.bind(service);

bound.thenAccept(binding -> {
System.out.println("gRPC server bound to: " + binding.localAddress());
});
return bound.toCompletableFuture().get();
}

@Test
@SuppressWarnings("unchecked")
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 {
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();

GrpcServiceException ex = GrpcServiceException.apply(statusRuntimeException);
RichMetadata meta = (RichMetadata) ex.getMetadata();
assertEquals("type.googleapis.com/google.rpc.LocalizedMessage", meta.getDetails().get(0).typeUrl());

assertEquals(Status.INVALID_ARGUMENT.getCode().value(), meta.getCode());
assertEquals("What is wrong?", meta.getMessage());

LocalizedMessage details = meta.getParsedDetails(0, com.google.rpc.error_details.LocalizedMessage.messageCompanion());
assertEquals("The password!", details.message());
assertEquals("EN", details.locale());
// #client_request

} catch (Exception e) {
e.printStackTrace();
Assert.fail("Got unexpected error " + e.getMessage());
} finally {
if (client != null) client.close();
sys.terminate();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ private CompletionStage<ServerBinding> run(ActorSystem sys) throws Exception {
GreeterService impl = new RichErrorImpl();

akka.japi.function.Function<HttpRequest, CompletionStage<HttpResponse>> service = GreeterServiceHandlerFactory.create(impl, sys);

return Http
.get(sys)
.newServerAt("127.0.0.1", 8090)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
import akka.stream.javadsl.Source;
import com.google.rpc.Code;
import com.google.rpc.error_details.LocalizedMessage;
import scala.collection.JavaConverters;
import scalapb.GeneratedMessage;

import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
Expand All @@ -25,10 +23,10 @@ public CompletionStage<HelloReply> sayHello(HelloRequest in) {
ArrayList<scalapb.GeneratedMessage> ar = new ArrayList<>();
ar.add(LocalizedMessage.of("EN", "The password!"));

GrpcServiceException exception = GrpcServiceException.apply(
GrpcServiceException exception = GrpcServiceException.create(
Code.INVALID_ARGUMENT,
"What is wrong?",
JavaConverters.asScalaBuffer(ar).toSeq()
ar
);

CompletableFuture<HelloReply> future = new CompletableFuture<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Copyright (C) 2020-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package akka.grpc.scaladsl

import akka.NotUsed
import akka.actor.ActorSystem
import akka.grpc.{ GrpcClientSettings, GrpcServiceException }
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{ HttpRequest, HttpResponse }
import akka.stream.scaladsl.{ Sink, Source }
import akka.testkit.TestKit
import com.google.rpc.Code
import com.google.rpc.error_details.LocalizedMessage
import com.typesafe.config.ConfigFactory
import example.myapp.helloworld.grpc.helloworld._
import org.scalatest.BeforeAndAfterAll
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.matchers.should.Matchers
import org.scalatest.time.Span
import org.scalatest.wordspec.AnyWordSpecLike

import scala.concurrent.duration._
import scala.concurrent.{ ExecutionContext, Future }

class RichErrorModelNativeSpec
extends TestKit(ActorSystem("RichErrorNativeSpec"))
with AnyWordSpecLike
with Matchers
with BeforeAndAfterAll
with ScalaFutures {

override implicit val patienceConfig: PatienceConfig = PatienceConfig(5.seconds, Span(10, org.scalatest.time.Millis))

implicit val sys: ActorSystem = system
implicit val ec: ExecutionContext = sys.dispatcher

object RichErrorNativeImpl extends GreeterService {

// #rich_error_model_unary
def sayHello(in: HelloRequest): Future[HelloReply] = {
Future.failed(
GrpcServiceException(Code.INVALID_ARGUMENT, "What is wrong?", Seq(new LocalizedMessage("EN", "The password!"))))
}
// #rich_error_model_unary

def itKeepsReplying(in: HelloRequest): Source[HelloReply, NotUsed] = {
Source.failed(
GrpcServiceException(Code.INVALID_ARGUMENT, "What is wrong?", Seq(new LocalizedMessage("EN", "The password!"))))
}

override def itKeepsTalking(in: Source[HelloRequest, NotUsed]): Future[HelloReply] = {
in.runWith(Sink.seq).flatMap { _ =>
Future.failed(
GrpcServiceException(
Code.INVALID_ARGUMENT,
"What is wrong?",
Seq(new LocalizedMessage("EN", "The password!"))))
}
}

override def streamHellos(in: Source[HelloRequest, NotUsed]): Source[HelloReply, NotUsed] = {
Source.failed(
GrpcServiceException(Code.INVALID_ARGUMENT, "What is wrong?", Seq(new LocalizedMessage("EN", "The password!"))))
}
}

val service: HttpRequest => Future[HttpResponse] =
GreeterServiceHandler(RichErrorNativeImpl)

val bound =
Http(system).newServerAt(interface = "127.0.0.1", port = 0).bind(service).futureValue

val client = GreeterServiceClient(
GrpcClientSettings.connectToServiceAt("127.0.0.1", bound.localAddress.getPort).withTls(false))

val conf = ConfigFactory.load().withFallback(ConfigFactory.defaultApplication())

"Rich error model" should {

"work with the native api on a unary call" in {

// #client_request
val richErrorResponse = client.sayHello(HelloRequest("Bob")).failed.futureValue

richErrorResponse match {
case status: GrpcServiceException =>
status.metadata match {
case richMetadata: RichMetadata =>
richMetadata.details(0).typeUrl should be("type.googleapis.com/google.rpc.LocalizedMessage")

import LocalizedMessage.messageCompanion
val localizedMessage: LocalizedMessage = richMetadata.getParsedDetails(0)
localizedMessage.message should be("The password!")
localizedMessage.locale should be("EN")

richMetadata.code should be(3)
richMetadata.message should be("What is wrong?")

case other => fail(s"This should be a RichGrpcMetadataImpl but it is ${other.getClass}")
}

case ex => fail(s"This should be a GrpcServiceException but it is ${ex.getClass}")
}
// #client_request
}

"work with the native api on a stream request" in {

val requests = List("Alice", "Bob", "Peter").map(HelloRequest(_))

val richErrorResponse = client.itKeepsTalking(Source(requests)).failed.futureValue

richErrorResponse match {
case status: GrpcServiceException =>
status.metadata match {
case metadata: RichMetadata =>
metadata.details(0).typeUrl should be("type.googleapis.com/google.rpc.LocalizedMessage")

import LocalizedMessage.messageCompanion
val localizedMessage: LocalizedMessage = metadata.getParsedDetails(0)

metadata.code should be(3)
metadata.message should be("What is wrong?")
localizedMessage.message should be("The password!")
localizedMessage.locale should be("EN")

case other => fail(s"This should be a RichGrpcMetadataImpl but it is ${other.getClass}")
}

case ex => fail(s"This should be a GrpcServiceException but it is ${ex.getClass}")
}

}

"work with the native api on a stream response" in {
val richErrorResponseStream = client.itKeepsReplying(HelloRequest("Bob"))
val richErrorResponse =
richErrorResponseStream.run().failed.futureValue

richErrorResponse match {
case status: GrpcServiceException =>
status.metadata match {
case metadata: RichMetadata =>
metadata.details(0).typeUrl should be("type.googleapis.com/google.rpc.LocalizedMessage")

val localizedMessage = metadata.getParsedDetails[LocalizedMessage](0)

metadata.code should be(3)
metadata.message should be("What is wrong?")
localizedMessage.message should be("The password!")
localizedMessage.locale should be("EN")

case other => fail(s"This should be a RichGrpcMetadataImpl but it is ${other.getClass}")
}
case ex => fail(s"This should be a GrpcServiceException but it is ${ex.getClass}")
}

}

"work with the native api on a bidi stream" in {

val requests = List("Alice", "Bob", "Peter").map(HelloRequest(_))
val richErrorResponseStream = client.streamHellos(Source(requests))
val richErrorResponse =
richErrorResponseStream.run().failed.futureValue

richErrorResponse match {
case status: GrpcServiceException =>
status.metadata match {
case metadata: RichMetadata =>
metadata.details(0).typeUrl should be("type.googleapis.com/google.rpc.LocalizedMessage")

val localizedMessage = metadata.getParsedDetails[LocalizedMessage](0)

metadata.code should be(3)
metadata.message should be("What is wrong?")
localizedMessage.message should be("The password!")
localizedMessage.locale should be("EN")

case other => fail(s"This should be a RichGrpcMetadataImpl but it is ${other.getClass}")
}
case ex => fail(s"This should be a GrpcServiceException but it is ${ex.getClass}")
}

}

}

override def afterAll(): Unit = system.terminate().futureValue
}
Loading