From 24d5d33d5b1436ef46f9be9c59e3a9d34289783d Mon Sep 17 00:00:00 2001 From: msosnicki Date: Wed, 12 Jul 2023 14:32:36 +0200 Subject: [PATCH 1/3] Error transformations as middleware --- .../http4s/ClientEndpointMiddleware.scala | 30 +++++++++- .../http4s/ServerEndpointMiddleware.scala | 57 +++++++++++++++++++ .../http4s/SimpleProtocolBuilder.scala | 39 +++++++++++-- .../smithy4s/http4s/SmithyHttp4sRouter.scala | 2 - .../SmithyHttp4sServerEndpoint.scala | 37 +++--------- 5 files changed, 128 insertions(+), 37 deletions(-) diff --git a/modules/http4s/src/smithy4s/http4s/ClientEndpointMiddleware.scala b/modules/http4s/src/smithy4s/http4s/ClientEndpointMiddleware.scala index 84f865662..8c6611182 100644 --- a/modules/http4s/src/smithy4s/http4s/ClientEndpointMiddleware.scala +++ b/modules/http4s/src/smithy4s/http4s/ClientEndpointMiddleware.scala @@ -18,12 +18,23 @@ package smithy4s package http4s import org.http4s.client.Client +import cats.kernel.Monoid // format: off -trait ClientEndpointMiddleware[F[_]] { +trait ClientEndpointMiddleware[F[_]] { + self => def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])( endpoint: Endpoint[service.Operation, _, _, _, _, _] ): Client[F] => Client[F] + + def andThen(other: ClientEndpointMiddleware[F]): ClientEndpointMiddleware[F] = + new ClientEndpointMiddleware[F] { + def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])( + endpoint: Endpoint[service.Operation, _, _, _, _, _] + ): Client[F] => Client[F] = + self.prepare(service)(endpoint).andThen(other.prepare(service)(endpoint)) + } + } // format: on @@ -48,4 +59,21 @@ object ClientEndpointMiddleware { ): Client[F] => Client[F] = identity } + implicit def monoidClientEndpointMiddleware[F[_]] + : Monoid[ClientEndpointMiddleware[F]] = + new Monoid[ClientEndpointMiddleware[F]] { + def combine( + a: ClientEndpointMiddleware[F], + b: ClientEndpointMiddleware[F] + ): ClientEndpointMiddleware[F] = + a.andThen(b) + + val empty: ClientEndpointMiddleware[F] = + new ClientEndpointMiddleware[F] { + def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])( + endpoint: Endpoint[service.Operation, _, _, _, _, _] + ): Client[F] => Client[F] = + identity + } + } } diff --git a/modules/http4s/src/smithy4s/http4s/ServerEndpointMiddleware.scala b/modules/http4s/src/smithy4s/http4s/ServerEndpointMiddleware.scala index ca3557a2f..a684efe3a 100644 --- a/modules/http4s/src/smithy4s/http4s/ServerEndpointMiddleware.scala +++ b/modules/http4s/src/smithy4s/http4s/ServerEndpointMiddleware.scala @@ -17,13 +17,27 @@ package smithy4s package http4s +import cats.Monoid +import cats.MonadThrow +import cats.data.Kleisli +import org.http4s.Response import org.http4s.HttpApp +import cats.implicits._ // format: off trait ServerEndpointMiddleware[F[_]] { + self => def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])( endpoint: Endpoint[service.Operation, _, _, _, _, _] ): HttpApp[F] => HttpApp[F] + + def andThen(other: ServerEndpointMiddleware[F]): ServerEndpointMiddleware[F] = + new ServerEndpointMiddleware[F] { + def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])( + endpoint: Endpoint[service.Operation, _, _, _, _, _] + ): HttpApp[F] => HttpApp[F] = + self.prepare(service)(endpoint).andThen(other.prepare(service)(endpoint)) + } } // format: on @@ -41,6 +55,32 @@ object ServerEndpointMiddleware { prepareWithHints(service.hints, endpoint.hints) } + def mapErrors[F[_]: MonadThrow]( + f: PartialFunction[Throwable, Throwable] + ): ServerEndpointMiddleware[F] = + flatMapErrors(f.andThen(_.pure[F])) + + def flatMapErrors[F[_]: MonadThrow]( + f: PartialFunction[Throwable, F[Throwable]] + ): ServerEndpointMiddleware[F] = + new ServerEndpointMiddleware[F] { + def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])( + endpoint: Endpoint[service.Operation, _, _, _, _, _] + ): HttpApp[F] => HttpApp[F] = http => { + val handler: PartialFunction[Throwable, F[Throwable]] = { + case e @ endpoint.Error(_, _) => e.raiseError[F, Throwable] + case scala.util.control.NonFatal(other) if f.isDefinedAt(other) => + f(other).flatMap(_.raiseError[F, Throwable]) + + } + Kleisli(req => + http(req).recoverWith( + handler.andThen(_.flatMap(_.raiseError[F, Response[F]])) + ) + ) + } + } + private[http4s] type EndpointMiddleware[F[_], Op[_, _, _, _, _]] = Endpoint[Op, _, _, _, _, _] => HttpApp[F] => HttpApp[F] @@ -51,4 +91,21 @@ object ServerEndpointMiddleware { ): HttpApp[F] => HttpApp[F] = identity } + implicit def monoidServerEndpointMiddleware[F[_]] + : Monoid[ServerEndpointMiddleware[F]] = + new Monoid[ServerEndpointMiddleware[F]] { + def combine( + a: ServerEndpointMiddleware[F], + b: ServerEndpointMiddleware[F] + ): ServerEndpointMiddleware[F] = + a.andThen(b) + + val empty: ServerEndpointMiddleware[F] = + new ServerEndpointMiddleware[F] { + def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])( + endpoint: Endpoint[service.Operation, _, _, _, _, _] + ): HttpApp[F] => HttpApp[F] = + identity + } + } } diff --git a/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala b/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala index 4e42d7c67..ee82b88b9 100644 --- a/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala +++ b/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala @@ -129,6 +129,23 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit val entityCompiler = EntityCompiler.fromCodecAPI(codecs) + /** + * Applies the error transformation to the errors that are not in the smithy spec (has no effect on errors from spec). + * Transformed errors raised in endpoint implementation will be observable from [[middleware]]. + * Errors raised in the [[middleware]] will be transformed too. + * + * The following two are equivalent: + * {{{ + * val handlerPF: PartialFunction[Throwable, Throwable] = ??? + * builder.mapErrors(handlerPF).middleware(middleware) + * }}} + + * {{{ + * val handlerPF: PartialFunction[Throwable, Throwable] = ??? + * val handler = ServerEndpointMiddleware.mapErrors(handlerPF) + * builder.middleware(handler |+| middleware |+| handler) + * }}} + */ def mapErrors( fe: PartialFunction[Throwable, Throwable] ): RouterBuilder[Alg, F] = @@ -137,7 +154,19 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit /** * Applies the error transformation to the errors that are not in the smithy spec (has no effect on errors from spec). * Transformed errors raised in endpoint implementation will be observable from [[middleware]]. - * Errors raised in the [[middleware]] will be transformed too. + * Errors raised in the [[middleware]] will be transformed too. + * + * The following two are equivalent: + * {{{ + * val handlerPF: PartialFunction[Throwable, F[Throwable]] = ??? + * builder.flatMapErrors(handlerPF).middleware(middleware) + * }}} + + * {{{ + * val handlerPF: PartialFunction[Throwable, F[Throwable]] = ??? + * val handler = ServerEndpointMiddleware.flatMapErrors(handlerPF) + * builder.middleware(handler |+| middleware |+| handler) + * }}} */ def flatMapErrors( fe: PartialFunction[Throwable, F[Throwable]] @@ -157,9 +186,11 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit new SmithyHttp4sRouter[Alg, service.Operation, F]( service, service.toPolyFunction[Kind1[F]#toKind5](impl), - errorTransformation, - entityCompiler, - middleware + entityCompiler, { + val errorHandler = + ServerEndpointMiddleware.flatMapErrors(errorTransformation) + errorHandler |+| middleware |+| errorHandler + } ).routes } diff --git a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala index 47cc0ec9e..c10074a1f 100644 --- a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala +++ b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala @@ -30,7 +30,6 @@ import cats.effect.SyncIO class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( service: smithy4s.Service.Aux[Alg, Op], impl: FunctorInterpreter[Op, F], - errorTransformation: PartialFunction[Throwable, F[Throwable]], entityCompiler: EntityCompiler[F], middleware: ServerEndpointMiddleware[F] )(implicit effect: EffectCompat[F]) { @@ -57,7 +56,6 @@ class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( impl, ep, compilerContext, - errorTransformation, middleware.prepare(service) _, pathParamsKey ) diff --git a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala index 600a10c32..3c7675ff9 100644 --- a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala +++ b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala @@ -56,7 +56,6 @@ private[http4s] object SmithyHttp4sServerEndpoint { impl: FunctorInterpreter[Op, F], endpoint: Endpoint[Op, I, E, O, SI, SO], compilerContext: CompilerContext[F], - errorTransformation: PartialFunction[Throwable, F[Throwable]], middleware: ServerEndpointMiddleware.EndpointMiddleware[F, Op], pathParamsKey: Key[PathParams] ): Either[ @@ -78,7 +77,6 @@ private[http4s] object SmithyHttp4sServerEndpoint { method, httpEndpoint, compilerContext, - errorTransformation, middleware, pathParamsKey ) @@ -94,7 +92,6 @@ private[http4s] class SmithyHttp4sServerEndpointImpl[F[_], Op[_, _, _, _, _], I, val method: Method, httpEndpoint: HttpEndpoint[I], compilerContext: CompilerContext[F], - errorTransformation: PartialFunction[Throwable, F[Throwable]], middleware: ServerEndpointMiddleware.EndpointMiddleware[F, Op], pathParamsKey: Key[PathParams] )(implicit F: EffectCompat[F]) extends SmithyHttp4sServerEndpoint[F] { @@ -107,14 +104,8 @@ private[http4s] class SmithyHttp4sServerEndpointImpl[F[_], Op[_, _, _, _, _], I, httpEndpoint.matches(path) } - private val applyMiddleware: HttpApp[F] => HttpApp[F] = { app => - middleware(endpoint)(app).handleErrorWith(error => - Kleisli.liftF(errorResponse(error)) - ) - } - - override val httpApp: HttpApp[F] = - httpAppErrorHandle(applyMiddleware(HttpApp[F] { req => + override val httpApp: HttpApp[F] = { + val baseApp = HttpApp[F] { req => val pathParams = req.attributes.lookup(pathParamsKey).getOrElse(Map.empty) val run: F[O] = for { @@ -123,18 +114,11 @@ private[http4s] class SmithyHttp4sServerEndpointImpl[F[_], Op[_, _, _, _, _], I, output <- (impl(endpoint.wrap(input)): F[O]) } yield output - run - .recoverWith(transformError) - .map(successResponse) - })) - - private def httpAppErrorHandle(app: HttpApp[F]): HttpApp[F] = { - app - .recoverWith { - case error if errorTransformation.isDefinedAt(error) => - Kleisli.liftF(errorTransformation.apply(error).flatMap(errorResponse)) - } - .handleErrorWith { error => Kleisli.liftF(errorResponse(error)) } + run.map(successResponse) + } + middleware(endpoint)(baseApp).handleErrorWith(error => + Kleisli.liftF(errorResponse(error)) + ) } private val inputSchema: Schema[I] = endpoint.input @@ -152,13 +136,6 @@ private[http4s] class SmithyHttp4sServerEndpointImpl[F[_], Op[_, _, _, _, _], I, : EntityEncoder[F, HttpContractError] = entityCompiler.compileEntityEncoder(HttpContractError.schema, entityCache) - private val transformError: PartialFunction[Throwable, F[O]] = { - case e @ endpoint.Error(_, _) => F.raiseError(e) - case scala.util.control.NonFatal(other) - if errorTransformation.isDefinedAt(other) => - errorTransformation(other).flatMap(F.raiseError) - } - private val extractInput: (Metadata, Request[F]) => F[I] = { inputMetadataDecoder.total match { case Some(totalDecoder) => From c4ba498c1f634c316dd881ac2dfdf121fce8d8f5 Mon Sep 17 00:00:00 2001 From: msosnicki Date: Wed, 12 Jul 2023 14:49:06 +0200 Subject: [PATCH 2/3] Fix bincompat --- .../src/smithy4s/http4s/SmithyHttp4sRouter.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala index c10074a1f..0900d8cf5 100644 --- a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala +++ b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala @@ -34,6 +34,18 @@ class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( middleware: ServerEndpointMiddleware[F] )(implicit effect: EffectCompat[F]) { + def this( + service: smithy4s.Service.Aux[Alg, Op], + impl: FunctorInterpreter[Op, F], + errorTransformation: PartialFunction[Throwable, F[Throwable]], + entityCompiler: EntityCompiler[F], + middleware: ServerEndpointMiddleware[F] + )(implicit effect: EffectCompat[F]) = + this(service, impl, entityCompiler, { + val errorHandler = ServerEndpointMiddleware.flatMapErrors(errorTransformation) + errorHandler |+| middleware |+| errorHandler + }) + private val pathParamsKey = Key.newKey[SyncIO, smithy4s.http.PathParams].unsafeRunSync() From c8a771fac5df2f4795b1cf010d132938cd4bb234 Mon Sep 17 00:00:00 2001 From: msosnicki Date: Wed, 12 Jul 2023 15:42:19 +0200 Subject: [PATCH 3/3] Fix http4s_CE22_12 issues --- modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala index 0900d8cf5..6c9bca3a3 100644 --- a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala +++ b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala @@ -42,7 +42,7 @@ class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( middleware: ServerEndpointMiddleware[F] )(implicit effect: EffectCompat[F]) = this(service, impl, entityCompiler, { - val errorHandler = ServerEndpointMiddleware.flatMapErrors(errorTransformation) + val errorHandler = ServerEndpointMiddleware.flatMapErrors(errorTransformation)(effect) errorHandler |+| middleware |+| errorHandler })