This is an automated email from the ASF dual-hosted git repository.

He-Pin pushed a commit to branch feat/service-info-in-status-exception
in repository https://gitbox.apache.org/repos/asf/pekko-grpc.git

commit 32ef6af25c6ec12475499efd8b81559ee288a04c
Author: 虎鸣 <[email protected]>
AuthorDate: Fri Jul 3 04:04:22 2026 +0800

    feat: include called service URI in StatusRuntimeException descriptions
    
    Motivation:
    When a gRPC call fails, the StatusRuntimeException description does not
    indicate which service/method was being called, making debugging harder
    in environments with multiple services.
    
    Modification:
    Add requestUri parameter to responseToSource, parseResponseStatus, and
    mapToStatusException. Use augmentDescription to append the request URI
    to all error descriptions. Update existing tests and add a dedicated
    test for URI inclusion.
    
    Result:
    StatusRuntimeException messages now include the full request URI
    (e.g. "When calling rpc service: https://host/Service/Method";),
    improving debuggability of gRPC failures.
    
    Tests:
    sbt "runtime / Test / testOnly 
org.apache.pekko.grpc.internal.PekkoHttpClientUtilsSpec" — 4/4 passed
    
    References:
    Refs akka/akka-grpc#1748
---
 .../pekko/grpc/internal/PekkoHttpClientUtils.scala | 32 +++++++++++++++-------
 .../grpc/internal/PekkoHttpClientUtilsSpec.scala   | 24 ++++++++++++++--
 2 files changed, 43 insertions(+), 13 deletions(-)

diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtils.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtils.scala
index 508ba6e3..a7ba5977 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtils.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtils.scala
@@ -185,7 +185,7 @@ object PekkoHttpClientUtils {
             descriptor.getFullMethodName),
           GrpcEntityHelpers.metadataHeaders(headers.entries),
           source)
-        responseToSource(singleRequest(httpRequest), deserializer)
+        responseToSource(httpRequest.uri, singleRequest(httpRequest), 
deserializer)
       }
     }
   }
@@ -194,7 +194,7 @@ object PekkoHttpClientUtils {
    * INTERNAL API
    */
   @InternalApi
-  def responseToSource[O](response: Future[HttpResponse], deserializer: 
ProtobufSerializer[O])(
+  def responseToSource[O](requestUri: Uri, response: Future[HttpResponse], 
deserializer: ProtobufSerializer[O])(
       implicit ec: ExecutionContext,
       mat: Materializer): Source[O, Future[GrpcResponseMetadata]] = {
     Source.lazyFutureSource[O, Future[GrpcResponseMetadata]](() => {
@@ -202,7 +202,7 @@ object PekkoHttpClientUtils {
         {
           if (response.status != StatusCodes.OK) {
             response.entity.discardBytes()
-            val failure = mapToStatusException(response, immutable.Seq.empty)
+            val failure = mapToStatusException(requestUri, response, 
immutable.Seq.empty)
             Source.failed(failure).mapMaterializedValue(_ => 
Future.failed(failure))
           } else {
             Codecs.detect(response) match {
@@ -211,7 +211,7 @@ object PekkoHttpClientUtils {
                 val trailerPromise = Promise[immutable.Seq[HttpHeader]]()
                 // Completed with success or failure based on grpc-status and 
grpc-message trailing headers
                 val completionFuture: Future[Unit] =
-                  trailerPromise.future.flatMap(trailers => 
parseResponseStatus(response, trailers))
+                  trailerPromise.future.flatMap(trailers => 
parseResponseStatus(requestUri, response, trailers))
 
                 val responseData =
                   response.entity match {
@@ -234,7 +234,7 @@ object PekkoHttpClientUtils {
                       Source.single[ByteString](data)
                     case _ =>
                       response.entity.discardBytes()
-                      throw mapToStatusException(response, Seq.empty)
+                      throw mapToStatusException(requestUri, response, 
Seq.empty)
                   }
                 responseData
                   // This never adds any data to the stream, but makes sure it 
fails with the correct error code if applicable
@@ -272,25 +272,37 @@ object PekkoHttpClientUtils {
     })
   }.mapMaterializedValue(_.flatten)
 
-  private def parseResponseStatus(response: HttpResponse, trailers: 
Seq[HttpHeader]): Future[Unit] = {
+  private def parseResponseStatus(requestUri: Uri, response: HttpResponse, 
trailers: Seq[HttpHeader]): Future[Unit] = {
     val allHeaders = response.headers ++ trailers
     allHeaders.find(_.name == "grpc-status").map(_.value) match {
       case Some("0") =>
         Future.successful(())
       case _ =>
-        Future.failed(mapToStatusException(response, trailers))
+        Future.failed(mapToStatusException(requestUri, response, trailers))
     }
   }
 
-  private def mapToStatusException(response: HttpResponse, trailers: 
Seq[HttpHeader]): StatusRuntimeException = {
+  private def mapToStatusException(
+      requestUri: Uri,
+      response: HttpResponse,
+      trailers: Seq[HttpHeader]): StatusRuntimeException = {
     val allHeaders = response.headers ++ trailers
     val metadata: io.grpc.Metadata = new MetadataImpl(new 
HeaderMetadataImpl(allHeaders).asList).toGoogleGrpcMetadata()
     allHeaders.find(_.name == "grpc-status").map(_.value) match {
       case None =>
-        new StatusRuntimeException(mapHttpStatus(response).withDescription("No 
grpc-status found"), metadata)
+        new StatusRuntimeException(
+          mapHttpStatus(response)
+            .withDescription("No grpc-status found")
+            .augmentDescription(s"When calling rpc service: 
${requestUri.toString()}"),
+          metadata)
       case Some(statusCode) =>
         val description = allHeaders.find(_.name == 
"grpc-message").map(_.value)
-        new 
StatusRuntimeException(Status.fromCodeValue(statusCode.toInt).withDescription(description.orNull),
 metadata)
+        new StatusRuntimeException(
+          Status
+            .fromCodeValue(statusCode.toInt)
+            .withDescription(description.orNull)
+            .augmentDescription(s"When calling rpc service: 
${requestUri.toString()}"),
+          metadata)
     }
   }
 
diff --git 
a/runtime/src/test/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtilsSpec.scala
 
b/runtime/src/test/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtilsSpec.scala
index b67a59ed..6e37e76b 100644
--- 
a/runtime/src/test/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtilsSpec.scala
+++ 
b/runtime/src/test/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtilsSpec.scala
@@ -35,16 +35,19 @@ class PekkoHttpClientUtilsSpec extends 
TestKit(ActorSystem()) with AnyWordSpecLi
   implicit val patience: PatienceConfig =
     PatienceConfig(5.seconds, Span(100, org.scalatest.time.Millis))
 
+  val testUri = Uri("https://example.com/test.Service/Method";)
+
   "The conversion from HttpResponse to Source" should {
     "map a strict 404 response to a failed stream" in {
       val response =
         Future.successful(HttpResponse(NotFound, entity = 
Strict(GrpcProtocolNative.contentType, ByteString.empty)))
-      val source = PekkoHttpClientUtils.responseToSource(response, null)
+      val source = PekkoHttpClientUtils.responseToSource(testUri, response, 
null)
 
       val failure = source.run().failed.futureValue
       failure shouldBe a[StatusRuntimeException]
       // 
https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
       failure.asInstanceOf[StatusRuntimeException].getStatus.getCode should 
be(Status.Code.UNIMPLEMENTED)
+      failure.asInstanceOf[StatusRuntimeException].getStatus.getDescription 
should include(testUri.toString())
     }
 
     "map a strict 200 response with non-0 gRPC error code to a failed stream" 
in {
@@ -54,11 +57,12 @@ class PekkoHttpClientUtilsSpec extends 
TestKit(ActorSystem()) with AnyWordSpecLi
         Nil
       val response =
         Future.successful(HttpResponse(OK, responseHeaders, 
Strict(GrpcProtocolNative.contentType, ByteString.empty)))
-      val source = PekkoHttpClientUtils.responseToSource(response, null)
+      val source = PekkoHttpClientUtils.responseToSource(testUri, response, 
null)
 
       val failure = source.run().failed.futureValue
       failure shouldBe a[StatusRuntimeException]
       failure.asInstanceOf[StatusRuntimeException].getStatus.getCode should 
be(Status.Code.FAILED_PRECONDITION)
+      failure.asInstanceOf[StatusRuntimeException].getStatus.getDescription 
should include(testUri.toString())
       failure.asInstanceOf[StatusRuntimeException].getTrailers.get(key) should 
be("custom-value-in-header")
     }
 
@@ -75,14 +79,28 @@ class PekkoHttpClientUtilsSpec extends 
TestKit(ActorSystem()) with AnyWordSpecLi
           Map.empty[AttributeKey[?], Any].updated(AttributeKeys.trailer, 
responseTrailers),
           Strict(GrpcProtocolNative.contentType, ByteString.empty),
           HttpProtocols.`HTTP/1.1`))
-      val source = PekkoHttpClientUtils.responseToSource(response, null)
+      val source = PekkoHttpClientUtils.responseToSource(testUri, response, 
null)
 
       val failure = source.run().failed.futureValue
       failure.asInstanceOf[StatusRuntimeException].getStatus.getCode should 
be(Status.Code.FAILED_PRECONDITION)
+      failure.asInstanceOf[StatusRuntimeException].getStatus.getDescription 
should include(testUri.toString())
       failure.asInstanceOf[StatusRuntimeException].getTrailers.get(key) should 
be("custom-trailer-value")
       failure.asInstanceOf[StatusRuntimeException].getTrailers.get(keyBin) 
should be(ByteString("custom-trailer-value"))
     }
 
+    "include the request URI in StatusRuntimeException description for HTTP 
errors" in {
+      val uri = 
Uri("https://myhost.example.com/com.example.MyService/DoSomething";)
+      val response =
+        Future.successful(HttpResponse(ServiceUnavailable, entity = 
Strict(GrpcProtocolNative.contentType, ByteString.empty)))
+      val source = PekkoHttpClientUtils.responseToSource(uri, response, null)
+
+      val failure = source.run().failed.futureValue
+      failure shouldBe a[StatusRuntimeException]
+      val sre = failure.asInstanceOf[StatusRuntimeException]
+      sre.getStatus.getDescription should include("myhost.example.com")
+      sre.getStatus.getDescription should include("When calling rpc service")
+    }
+
     lazy val key = Metadata.Key.of("custom-key", 
Metadata.ASCII_STRING_MARSHALLER)
     lazy val keyBin = Metadata.Key.of("custom-key-bin", 
Metadata.BINARY_BYTE_MARSHALLER)
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to