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

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-grpc.git


The following commit(s) were added to refs/heads/main by this push:
     new 3dd4c682 Do not return NotImplementedError messages to the caller 
(#874)
3dd4c682 is described below

commit 3dd4c68219394fe2f64da86702e309063ea092e1
Author: PJ Fanning <[email protected]>
AuthorDate: Tue Sep 1 10:07:48 2026 +0100

    Do not return NotImplementedError messages to the caller (#874)
    
    Motivation:
    `defaultMapper` logged and scrubbed the message of every failure it did not
    recognise, but two cases were different:
    
        case e: NotImplementedError           => 
Trailers(Status.UNIMPLEMENTED.withDescription(e.getMessage))
        case e: UnsupportedOperationException => 
Trailers(Status.UNIMPLEMENTED.withDescription(e.getMessage))
    
    Those messages come from whatever the service implementation threw, so an
    exception raised deep inside a handler put its text on the wire. Neither 
case was
    logged either, so the detail went to the caller and nowhere else - the 
opposite
    of every other branch.
    
    `???` in Scala throws `NotImplementedError`, and 
`UnsupportedOperationException`
    comes out of many collection and stream operations, so these are easy to hit
    without meaning to report anything.
    
    Modification:
    Both cases now log at warning and answer with a bare `UNIMPLEMENTED`, 
matching
    how the mapper treats every other unrecognised failure, in both the 
scaladsl and
    javadsl handlers.
    
    `GrpcServiceException` remains the way to send a description deliberately, 
so the
    generated handlers now raise that for an unknown method rather than relying 
on
    the message of a `NotImplementedError` reaching the caller. The behaviour a
    client sees for an unknown method is unchanged.
    
    Result:
    An accidental `???` or unsupported collection operation no longer describes
    itself to the caller, and the detail is logged where an operator can see it.
    Deliberate descriptions still work.
    
    Tests:
    - 3 new cases in `GrpcExceptionHandlerSpec`: the message of each exception 
type
      is withheld, and a deliberate `GrpcServiceException` description still 
arrives
    - The existing table-driven expectation is updated, which is the behaviour 
change
    - The existing end-to-end `ErrorReportingSpec` in both plugin-testers 
already
      asserts a client calling an unknown method receives
      `grpc-message: Not implemented: UnknownMethod`; both still pass, which is 
what
      covers the codegen half of this
    - Confirmed the guard bites: restoring the leak fails 3 tests
    - sbt "runtime/testOnly ...GrpcExceptionHandlerSpec" - 29 passed
    - sbt "plugin-tester-scala/testOnly ...ErrorReportingSpec" - 2 passed
    - sbt "plugin-tester-java/testOnly ...ErrorReportingSpec" - 2 passed
    - sbt "runtime/mimaReportBinaryIssues" - passed
    - sbt scalafmtAll scalafmtSbt - applied
    - sbt "runtime/test" - not run locally, left to CI
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../twirl/templates/JavaServer/Handler.scala.txt   |  2 +-
 .../twirl/templates/ScalaServer/Handler.scala.txt  |  4 +--
 .../pekko/grpc/javadsl/GrpcExceptionHandler.scala  | 16 +++++++---
 .../pekko/grpc/scaladsl/GrpcExceptionHandler.scala | 16 +++++++---
 .../grpc/scaladsl/GrpcExceptionHandlerSpec.scala   | 36 ++++++++++++++++++++--
 5 files changed, 60 insertions(+), 14 deletions(-)

diff --git a/codegen/src/main/twirl/templates/JavaServer/Handler.scala.txt 
b/codegen/src/main/twirl/templates/JavaServer/Handler.scala.txt
index 18194324..3f2ad445 100644
--- a/codegen/src/main/twirl/templates/JavaServer/Handler.scala.txt
+++ b/codegen/src/main/twirl/templates/JavaServer/Handler.scala.txt
@@ -218,7 +218,7 @@ public class @{serviceName}HandlerFactory {
           }
           default:
             
CompletableFuture<org.apache.pekko.http.javadsl.model.HttpResponse> result = 
new CompletableFuture<>();
-            result.completeExceptionally(new 
UnsupportedOperationException("Not implemented: " + method));
+            result.completeExceptionally(new 
org.apache.pekko.grpc.GrpcServiceException(io.grpc.Status.UNIMPLEMENTED.withDescription("Not
 implemented: " + method)));
             response = result;
         }
         return response.exceptionally(e -> GrpcExceptionHandler.standard(e, 
eHandler, writer, system));
diff --git a/codegen/src/main/twirl/templates/ScalaServer/Handler.scala.txt 
b/codegen/src/main/twirl/templates/ScalaServer/Handler.scala.txt
index 130c663a..d735d657 100644
--- a/codegen/src/main/twirl/templates/ScalaServer/Handler.scala.txt
+++ b/codegen/src/main/twirl/templates/ScalaServer/Handler.scala.txt
@@ -140,7 +140,7 @@ object @{serviceName}Handler {
                   
.recoverWith(GrpcExceptionHandler.from(eHandler(system.classicSystem))(system, 
writer))
                 }
             }
-            case m => 
GrpcExceptionHandler.from(eHandler(system.classicSystem))(system, writer)(new 
NotImplementedError(s"Not implemented: $m"))
+            case m => 
GrpcExceptionHandler.from(eHandler(system.classicSystem))(system, writer)(new 
pekko.grpc.GrpcServiceException(io.grpc.Status.UNIMPLEMENTED.withDescription(s"Not
 implemented: $m")))
           }
       ).getOrElse(unsupportedMediaType)
 
@@ -185,7 +185,7 @@ object @{serviceName}Handler {
                   
.recoverWith(GrpcExceptionHandler.from(eHandler(system.classicSystem))(system, 
writer))
                 }
             }
-            case m => 
GrpcExceptionHandler.from(eHandler(system.classicSystem))(system, writer)(new 
NotImplementedError(s"Not implemented: $m"))
+            case m => 
GrpcExceptionHandler.from(eHandler(system.classicSystem))(system, writer)(new 
pekko.grpc.GrpcServiceException(io.grpc.Status.UNIMPLEMENTED.withDescription(s"Not
 implemented: $m")))
           }
       ).getOrElse(unsupportedMediaType)
 
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/javadsl/GrpcExceptionHandler.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/javadsl/GrpcExceptionHandler.scala
index e3287087..05ef05f2 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/javadsl/GrpcExceptionHandler.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/javadsl/GrpcExceptionHandler.scala
@@ -35,6 +35,7 @@ import scala.concurrent.ExecutionException
 object GrpcExceptionHandler {
   private val INTERNAL = Trailers(Status.INTERNAL)
   private val INVALID_ARGUMENT = Trailers(Status.INVALID_ARGUMENT)
+  private val UNIMPLEMENTED = Trailers(Status.UNIMPLEMENTED)
 
   def defaultMapper: jFunction[ActorSystem, jFunction[Throwable, Trailers]] =
     new jFunction[ActorSystem, jFunction[Throwable, Trailers]] {
@@ -58,10 +59,17 @@ object GrpcExceptionHandler {
             else default(system)(e.getCause)
           case grpcException: GrpcServiceException => 
Trailers(grpcException.status, grpcException.metadata)
           case _: MissingParameterException        => INVALID_ARGUMENT
-          case e: NotImplementedError              => 
Trailers(Status.UNIMPLEMENTED.withDescription(e.getMessage))
-          case e: UnsupportedOperationException    => 
Trailers(Status.UNIMPLEMENTED.withDescription(e.getMessage))
-          case e: StatusRuntimeException           => Trailers(e.getStatus, 
new GrpcMetadataImpl(e.getTrailers))
-          case e: PeerClosedStreamException        =>
+          case e: NotImplementedError              =>
+            // the message is whatever the service implementation happened to 
throw with, so it
+            // is logged rather than returned. Throw a GrpcServiceException to 
send a description
+            // on purpose; that is what the generated handlers do for an 
unknown method.
+            log(system).warning(e, "Unimplemented: [{}]", e.getMessage)
+            UNIMPLEMENTED
+          case e: UnsupportedOperationException =>
+            log(system).warning(e, "Unimplemented: [{}]", e.getMessage)
+            UNIMPLEMENTED
+          case e: StatusRuntimeException    => Trailers(e.getStatus, new 
GrpcMetadataImpl(e.getTrailers))
+          case e: PeerClosedStreamException =>
             log(system).warning(e, "Peer closed the stream: [{}]", 
e.getMessage)
             INTERNAL
           case other =>
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandler.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandler.scala
index bf84958f..ab60d298 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandler.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandler.scala
@@ -31,6 +31,7 @@ import pekko.event.Logging
 object GrpcExceptionHandler {
   private val INTERNAL = Trailers(Status.INTERNAL)
   private val INVALID_ARGUMENT = Trailers(Status.INVALID_ARGUMENT)
+  private val UNIMPLEMENTED = Trailers(Status.UNIMPLEMENTED)
 
   private def log(system: ActorSystem) = Logging(system, 
"org.apache.pekko.grpc.scaladsl.GrpcExceptionHandler")
 
@@ -39,10 +40,17 @@ object GrpcExceptionHandler {
       if (e.getCause == null) INTERNAL
       else defaultMapper(system)(e.getCause)
     case grpcException: GrpcServiceException => Trailers(grpcException.status, 
grpcException.metadata)
-    case e: NotImplementedError              => 
Trailers(Status.UNIMPLEMENTED.withDescription(e.getMessage))
-    case e: UnsupportedOperationException    => 
Trailers(Status.UNIMPLEMENTED.withDescription(e.getMessage))
-    case _: MissingParameterException        => INVALID_ARGUMENT
-    case e: StatusRuntimeException           =>
+    case e: NotImplementedError              =>
+      // the message is whatever the service implementation happened to throw 
with, so it is
+      // logged rather than returned. Throw a GrpcServiceException to send a 
description on
+      // purpose; that is what the generated handlers do for an unknown method.
+      log(system).warning(e, "Unimplemented: [{}]", e.getMessage)
+      UNIMPLEMENTED
+    case e: UnsupportedOperationException =>
+      log(system).warning(e, "Unimplemented: [{}]", e.getMessage)
+      UNIMPLEMENTED
+    case _: MissingParameterException => INVALID_ARGUMENT
+    case e: StatusRuntimeException    =>
       val meta = Option(e.getTrailers).getOrElse(new io.grpc.Metadata())
       Trailers(e.getStatus, new GrpcMetadataImpl(meta))
     case e: PeerClosedStreamException =>
diff --git 
a/runtime/src/test/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandlerSpec.scala
 
b/runtime/src/test/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandlerSpec.scala
index 92936b04..553de1a6 100644
--- 
a/runtime/src/test/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandlerSpec.scala
+++ 
b/runtime/src/test/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandlerSpec.scala
@@ -41,9 +41,11 @@ class GrpcExceptionHandlerSpec extends AnyWordSpec with 
Matchers with ScalaFutur
       else expected(e.getCause)
     case grpcException: GrpcServiceException => grpcException.status
     case e: StatusRuntimeException           => e.getStatus
-    case e: NotImplementedError              => 
Status.UNIMPLEMENTED.withDescription(e.getMessage)
-    case e: UnsupportedOperationException    => 
Status.UNIMPLEMENTED.withDescription(e.getMessage)
-    case _                                   => Status.INTERNAL
+    // the message is not returned to the caller: it is whatever the service 
implementation
+    // happened to throw with, so it is logged instead
+    case _: NotImplementedError           => Status.UNIMPLEMENTED
+    case _: UnsupportedOperationException => Status.UNIMPLEMENTED
+    case _                                => Status.INTERNAL
   }
 
   val otherTypes: Seq[Throwable] = Seq(
@@ -70,6 +72,34 @@ class GrpcExceptionHandlerSpec extends AnyWordSpec with 
Matchers with ScalaFutur
     }
   }
 
+  "defaultMapper" should {
+
+    "not return the message of a NotImplementedError to the caller" in {
+      val status = defaultMapper(system)(new NotImplementedError("internal 
detail")).status
+
+      status.getCode shouldBe Status.UNIMPLEMENTED.getCode
+      status.getDescription shouldBe null
+    }
+
+    "not return the message of an UnsupportedOperationException to the caller" 
in {
+      val status = defaultMapper(system)(new 
UnsupportedOperationException("internal detail")).status
+
+      status.getCode shouldBe Status.UNIMPLEMENTED.getCode
+      status.getDescription shouldBe null
+    }
+
+    "still return a description that was set deliberately" in {
+      // a GrpcServiceException is how a service says what to tell the caller, 
and is what the
+      // generated handlers raise for an unknown method
+      val deliberate = new 
GrpcServiceException(Status.UNIMPLEMENTED.withDescription("Not implemented: 
SayHello"))
+
+      val status = defaultMapper(system)(deliberate).status
+
+      status.getCode shouldBe Status.UNIMPLEMENTED.getCode
+      status.getDescription shouldBe "Not implemented: SayHello"
+    }
+  }
+
   "default(defaultMapper)" should {
     (otherTypes ++ executionExceptions).foreach { e =>
       s"Correctly map $e" in {


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

Reply via email to