Skip to content

Commit

Permalink
Merge pull request #1084 from msosnicki/error-handling-middleware-0.17
Browse files Browse the repository at this point in the history
Error transformations as middleware
  • Loading branch information
Baccata authored Jul 13, 2023
2 parents 18293ec + c8a771f commit 1b9f8fa
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
}
}
57 changes: 57 additions & 0 deletions modules/http4s/src/smithy4s/http4s/ServerEndpointMiddleware.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]

Expand All @@ -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
}
}
}
39 changes: 35 additions & 4 deletions modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand All @@ -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]]
Expand All @@ -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
}

Expand Down
14 changes: 12 additions & 2 deletions modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,22 @@ 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]) {

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)(effect)
errorHandler |+| middleware |+| errorHandler
})

private val pathParamsKey =
Key.newKey[SyncIO, smithy4s.http.PathParams].unsafeRunSync()

Expand All @@ -57,7 +68,6 @@ class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]](
impl,
ep,
compilerContext,
errorTransformation,
middleware.prepare(service) _,
pathParamsKey
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand All @@ -78,7 +77,6 @@ private[http4s] object SmithyHttp4sServerEndpoint {
method,
httpEndpoint,
compilerContext,
errorTransformation,
middleware,
pathParamsKey
)
Expand All @@ -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] {
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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) =>
Expand Down

0 comments on commit 1b9f8fa

Please sign in to comment.