Middleware examples #513
-
Hi :) I am trying out smithy4s and am not sure how to best go about enriching client requests with out-of-band headers with request info. @daddykotex has an example here: object HelloWorldImpl extends HelloWorldService[IO] {
def hello(name: String, town: Option[String]): IO[Greeting] = IO.pure {
town match {
case None => Greeting(s"Hello " + name + "!")
case Some(t) => Greeting(s"Hello " + name + " from " + t + "!")
}
}
}
object Routes {
private val example: Resource[IO, HttpRoutes[IO]] =
SimpleRestJsonBuilder.routes(HelloWorldImpl).resource
private val docs: HttpRoutes[IO] =
smithy4s.http4s.swagger.docs[IO](HelloWorldService)
val all: Resource[IO, HttpRoutes[IO]] = example.map(_ <+> docs)
}
object Main extends IOApp.Simple {
// client middleware
val authMiddleware: org.http4s.client.Middleware[IO] = { client =>
Client { req =>
client.run(
req.withHeaders(
headers.Authorization(Credentials.Token(AuthScheme.Bearer, "TOKEN"))
)
)
}
}
val client = EmberClientBuilder.default[IO].build.map { client =>
SimpleRestJsonBuilder(HelloWorldService)
.clientResource(
authMiddleware(client),
Uri.unsafeFromString("http://some-api.com")
)
}
val run = (client, Routes.all).tupled.flatMap { case (client, routes) =>
// here you have a HelloWorldClient, but headers will be added
EmberServerBuilder
.default[IO]
.withPort(port"9000")
.withHost(host"localhost")
.withHttpApp(routes.orNotFound)
.build
}.useForever
} However, that example does not require out of band input from the user. In a real world example I could imagine having services that require some sort of call context with audit & auth information on it pr. request, e.g.: HelloWorldService[F, C] {
def hello1(name: String): C => F[Greeting]
def hello2(name: String): C => F[Greeting]
def hello3(name: String): C => F[Greeting]
} So, Kleisli and FiberLocal come to mind, but I am a bit uncertain how to go about this that fits well with the generated code. def enrichHelloWorld[F[_], C](embed: C => Request[F]): HelloWorldService[Kleisli[F, C, *]] = ??? Very interested in hearing if there is some smart way, e.g. natural transformation, to achieve this using the generated code . The server side is a bit easier to go about when using def withRequestInfo[F[_]: MonadThrow](routes: HttpRoutes[F], setFn: Option[RequestInfo] => F[Unit]): HttpRoutes[F] =
HttpRoutes[F] { request =>
val requestInfo = for {
contentType <-
request.headers.get[`Content-Type`].map(ct => s"${ct.mediaType.mainType}/${ct.mediaType.subType}")
} yield RequestInfo(contentType)
OptionT.liftF(setFn(requestInfo)) *> routes(request)
} and correspondingly inject |
Beta Was this translation helpful? Give feedback.
Replies: 6 comments 7 replies
-
The issue was fine, but it's up to you. Thanks for bringing this up, and I'm still thinking about a good answer to your question. To be sure I understand correctly. When you say: "enriching client requests with out-of-band headers with request info." Do you mean that, in my example, the token is static (and hardcoded) where as you'd like to supply a potentially different value on each request? |
Beta Was this translation helpful? Give feedback.
-
Using IOLocal, the approach for the client-side is essentially the exact dual of the one we document for the server side. Essentially you'd have a middleware implementing that following signature, responsible for reading context out of the local and writing the headers (when the server-side does the exact opposite, it reads from the headers and writes the context into the local)
then you'd wrap the class MyHelloWorld(write: Context => IO[Unit]) extends HelloWorldService[IO] {
//...
} You can achieve a similar thing with Kleisli, in that the generated polymorphic type ContextIO[A] = Kleisli[IO, Context, A]
object MyHelloWorld extends HelloWorldService[ContextIO] {
// ...
} Cross-cutting concerns should not be reified at the Smithy-level
Right, so essentially there's a small discrepancy between the two points of view about what is a "specification" :
If all of your endpoints require some sort of similar information, be that tracing headers or auth keys, chances are those are PROTOCOL-LEVEL concern that should not be specified in the "input for code-generators" smithy spec. Typically what would happen is that you'd trust middlewares to take care of this for you, and if you really want for this information to happen in your "documentation for external callers", then you'd take care of that separately by having some build-time transformation pipeline that would take your service specification and produce the necessary documentation for the eyes of external parties (which may include the mixin). But the mixin you're thinking of should NOT be present in the specification that you use for the smithy4s code-generation, because you end up with the cross-cutting concerns polluting your domain-specific logic. Answering your question is hardFrom your question, it is unclear whether the cross-cutting contextual information that is used to fulfil outgoing requests comes from your application itself, or from incoming requests targeting your application. Chances are the If you want for your service implementation to have the capability to AMEND the contextual information with ad-hoc bits and pieces, then you need to put some thoughts in the abstraction that let you write into the |
Beta Was this translation helpful? Give feedback.
-
Thanks a lot for the answers :) I appreciate it. @Baccata I think this is probably the best approach.
I am fine with how to do server side and mostly concerned about the client part. When you write: class MyHelloWorld(write: Context => IO[Unit]) extends HelloWorldService[IO] {
//...
} and type ContextIO[A] = Kleisli[IO, Context, A]
object MyHelloWorld extends HelloWorldService[ContextIO] {
// ...
} Is that not for the server side? A bit confused with your SimpleRestJsonBuilder(HelloWorldService).client(client).uri(uri).resource So, I would need to do: def enrichHello[F[_]: Monad](
inner: HelloWorldService[F],
setInfo: RequestInfo => F[Unit]
): HelloWorldService[Kleisli[F, RequestInfo, *]] =
new HelloWorldService[Kleisli[F, RequestInfo, *]] {
def hello(name: String, town: Option[String]): Kleisli[F, RequestInfo, HelloOutput] =
Kleisli(setInfo(_) *> inner.hello(name, town))
} A bit much boilerplate. I was thinking if it was somehow possible to use the initial alg and have a transformation like ctx => op => setInfo(ctx) *> initial.run(op) Because I do not care about what op it is, just need to apply the ctx and otherwise run the endpoint operation. As that pattern seems to be generic it would be nice to have something like: val inner: HelloWorldService[F] = ???
val enriched: HelloWorldService[Kleisli[F, RequestInfo, *] = inner.enrich(setInfo) where So it seems that your suggestion does not really give me much. I can do a wrapper my self like: trait HelloWorld[F[_], C]:
def hello(name: String, town: Option[String]): C => F[HelloOutput]
object HelloWorld:
def apply[F[_]: Monad, C](smithyHello: HelloWorldService[F], setInfo: C => F[Unit]): HelloWorld[F, C] =
new HelloWorld[F, C]:
def hello(name: String, town: Option[String]): C => F[HelloOutput] =
setInfo(_) *> smithyHello.hello(name, town) The same repetition and more boilerplate, sigh. |
Beta Was this translation helpful? Give feedback.
-
Awesome @Baccata 👍 I have no issue with lifting mono-functors if it saves me boiler-plate :) |
Beta Was this translation helpful? Give feedback.
-
Btw, I think I found an issue in smithy4s. In the example that @daddykotex has, I modified the example a bit: package com.example
import smithy4s.hello._
import cats.effect._
import cats.implicits._
import org.http4s.implicits._
import org.http4s.ember.server._
import org.http4s._
import com.comcast.ip4s._
import smithy4s.http4s.SimpleRestJsonBuilder
import org.http4s.ember.client.EmberClientBuilder
import org.http4s.client.Client
object HelloWorldImpl extends HelloWorldService[IO] {
def hello(name: String, town: Option[String]): IO[Greeting] = IO.pure {
town match {
case None => Greeting(s"Hello " + name + "!")
case Some(t) => Greeting(s"Hello " + name + " from " + t + "!")
}
}
}
object Routes {
private val example: Resource[IO, HttpRoutes[IO]] =
SimpleRestJsonBuilder.routes(HelloWorldImpl).resource
private val docs: HttpRoutes[IO] =
smithy4s.http4s.swagger.docs[IO](HelloWorldService)
val all: Resource[IO, HttpRoutes[IO]] = example.map(_ <+> docs)
}
object Main extends IOApp.Simple {
// client middleware
val authMiddleware: org.http4s.client.Middleware[IO] = { client =>
Client { req =>
client.run(
req.withHeaders(
headers.Authorization(Credentials.Token(AuthScheme.Bearer, "TOKEN"))
)
)
}
}
val client = EmberClientBuilder.default[IO].build.map { client =>
SimpleRestJsonBuilder(HelloWorldService)
.clientResource(
authMiddleware(client),
Uri.unsafeFromString("http://127.0.0.1:9000")
)
}
val run = (client, Routes.all).tupled.flatMap { case (client, routes) =>
for {
hs <- client
_ <- EmberServerBuilder
.default[IO]
.withPort(port"9000")
.withHost(host"0.0.0.0")
.withHttpApp(routes.orNotFound)
.build
_ <- {
Resource.eval(hs.hello("Olivier").flatTap(IO.println)) *>
Resource.eval(hs.hello("David").flatTap(IO.println)) *>
Resource.eval(hs.hello("Jakub").flatTap(IO.println))
}
} yield ()
}.useForever
} I get this error: Greeting(Hello Olivier!)
[error] java.io.IOException: Broken pipe
[error] at java.base/sun.nio.ch.FileDispatcherImpl.write0(Native Method) The smithy spec is:
If I change
Then I get this: [info] running com.example.Main
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Greeting(Hello Olivier!)
Greeting(Hello David!)
Greeting(Hello Jakub!)
^C So, it seems that the payload is |
Beta Was this translation helpful? Give feedback.
-
@Baccata sure thing. |
Beta Was this translation helpful? Give feedback.
Using IOLocal, the approach for the client-side is essentially the exact dual of the one we document for the server side.
Essentially you'd have a middleware implementing that following signature, responsible for reading context out of the local and writing the headers (when the server-side does the exact opposite, it reads from the headers and writes the context into the local)
then you'd wrap the
IOLocal
in some abstraction that would allow for writing to it. Could be a mereContext => IO[Unit]
, and you pass that to whatever layer of your application has a handle to (or the ability to instantiate) aContext
.class