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-http.git
The following commit(s) were added to refs/heads/main by this push:
new da54f5efe fix: fail instead of throw when a transform on a completed
FastFuture throws (#1305)
da54f5efe is described below
commit da54f5efe295e4260f2c414b0bafb8fd7e00004c
Author: PJ Fanning <[email protected]>
AuthorDate: Thu Sep 17 16:23:39 2026 +0100
fix: fail instead of throw when a transform on a completed FastFuture
throws (#1305)
Motivation:
`FastFuture.successful` / `FastFuture.failed` return already completed
futures
that override `Future.transform` to apply the function directly. Unlike
`DefaultPromise`, that override did not catch exceptions thrown by the
function, so the standard `filter`, `collect` and `transform` combinators on
such a future threw synchronously at the call site instead of returning a
failed future. Callers cannot tell a FastFuture-produced `Future` from any
other, and pekko-grpc now returns them from its public APIs.
Modification:
Route both `transform` overrides through a helper that converts a `NonFatal`
exception from the function into an `ErrorFuture`, matching the contract of
`Future.transform`.
Result:
Standard combinators on a completed FastFuture return a failed future when
their function throws, the same as for `Future.successful` /
`Future.failed`.
Tests:
- sbt "http-core/Test/testOnly
org.apache.pekko.http.scaladsl.util.FastFutureSpec"
- new transform/filter/collect cases fail without the fix, 32/32 pass
with it
- sbt "http-core/mimaReportBinaryIssues" - no issues (against 1.0.0)
- scalafmt --mode diff-ref=upstream/main
- git diff --check
References:
None - found while reviewing FastFuture stack and exception semantics for
pekko-grpc
---
.../pekko/http/scaladsl/util/FastFuture.scala | 12 +++++++--
.../pekko/http/scaladsl/util/FastFutureSpec.scala | 31 ++++++++++++++++++++++
2 files changed, 41 insertions(+), 2 deletions(-)
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/util/FastFuture.scala
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/util/FastFuture.scala
index ca5dc1f20..bb1807626 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/util/FastFuture.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/util/FastFuture.scala
@@ -90,7 +90,7 @@ object FastFuture {
def ready(atMost: Duration)(implicit permit: CanAwait) = this
def transform[S](f: scala.util.Try[A] => scala.util.Try[S])(
implicit executor: scala.concurrent.ExecutionContext):
scala.concurrent.Future[S] =
- FastFuture(f(Success(a)))
+ strictTransform(Success(a), f)
def transformWith[S](f: scala.util.Try[A] => scala.concurrent.Future[S])(
implicit executor: scala.concurrent.ExecutionContext):
scala.concurrent.Future[S] =
new FastFuture(this).transformWith(f)
@@ -103,12 +103,20 @@ object FastFuture {
def ready(atMost: Duration)(implicit permit: CanAwait) = this
def transform[S](f: scala.util.Try[Nothing] => scala.util.Try[S])(
implicit executor: scala.concurrent.ExecutionContext):
scala.concurrent.Future[S] =
- FastFuture(f(Failure(error)))
+ strictTransform(Failure(error), f)
def transformWith[S](f: scala.util.Try[Nothing] =>
scala.concurrent.Future[S])(
implicit executor: scala.concurrent.ExecutionContext):
scala.concurrent.Future[S] =
new FastFuture(this).transformWith(f)
}
+ /**
+ * Applies `f` to an already available result, turning an exception thrown
by `f` into a failed future
+ * the way [[scala.concurrent.Future.transform]] does for asynchronously
completed futures.
+ */
+ private def strictTransform[A, S](value: Try[A], f: Try[A] => Try[S]):
Future[S] =
+ try FastFuture(f(value))
+ catch { case NonFatal(e) => ErrorFuture(e) }
+
implicit class EnhancedFuture[T](val future: Future[T]) extends AnyVal {
def fast: FastFuture[T] = new FastFuture[T](future)
}
diff --git
a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/util/FastFutureSpec.scala
b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/util/FastFutureSpec.scala
index b8b78b4c1..fb3e0e474 100644
---
a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/util/FastFutureSpec.scala
+++
b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/util/FastFutureSpec.scala
@@ -171,6 +171,37 @@ class FastFutureSpec extends AnyFreeSpec with Matchers {
}
}
+ "FastFuture-produced futures should fail instead of throwing when a standard
Future combinator's function throws" - {
+ // The already completed futures override `Future.transform`, which the
standard `filter`, `collect` and
+ // `transform` combinators are built on, so an exception thrown by the
user function must become a
+ // failed future exactly as it does for a `Future.successful` /
`Future.failed` value.
+ "transform on a successful future" in {
+ testStdlib(Success(23), _.transform(_ => throw TheException))
+ }
+ "transform on a failed future" in {
+ testStdlib(Failure(UnexpectedException), _.transform(_ => throw
TheException))
+ }
+ "filter with a throwing predicate" in {
+ testStdlib(Success(23), _.filter(_ => throw TheException))
+ }
+ "collect with a throwing partial function" in {
+ testStdlib(Success(23), _.collect { case _ => throw TheException })
+ }
+ "map with a throwing function" in {
+ testStdlib(Success(23), _.map(_ => throw TheException))
+ }
+ "recover with a throwing partial function" in {
+ testStdlib(Failure(UnexpectedException), _.recover { case _ => throw
TheException })
+ }
+ }
+
+ def testStdlib(result: Try[Int], op: Future[Int] => Future[Int]): Unit = {
+ val f = FastFuture(result)
+ val transformed = op(f)
+ Await.ready(transformed, 500.millis)
+ transformed.value shouldEqual Some(Failure(TheException))
+ }
+
def test(result: Try[Int], op: FastFuture[Int] => Future[Int])(check:
Try[Int] => Unit): Unit = {
def testStrictly(): Unit = {
val f = FastFuture(result)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]