Interceptors and attributes in zio2/scala3/ziohttp

Ok, what sounds like a super simple issue. I have a method:

def enforcePermission(permission: Permission): ZIO[SessionContext, PermissionError, Unit]

I’d like to add to my endpoint an attribute:

endpoint.attribute(AttributeKey[Permission], aPermission)

And then, (I think), have an interceptor that picks out my user from the SessionContext from the environment, the permission set in the attribute, and enforce (fail with PermissionError) the permission given the user.
The types are giving me a headache. I think I have it close, but would like some help to make the types work.
I create options with:

  ZioHttpServerOptions.default[SessionContext].prependInterceptor {
    permissionInterceptor
  }

But then I’m not sure how to declare the permissionInterceptor, intelliJ’s helper keeps on insisting I need a double equals arrow (==>) operator that I have no idea what it means, but when I try to create that it doesn’t match what it expects.

What’s really confusing is that this:

  def myInterceptor: Interceptor[[_] =>> RIO[SessionContext, _]] = ???

  ZioHttpServerOptions.default[SessionContext].prependInterceptor(myInterceptor)

Comes back with

Found:    sttp.tapir.server.interceptor.Interceptor[
  [_] =>> zio.ZIO[gateway.model.auth.SessionContext, Throwable, ?]]
Required: sttp.tapir.server.interceptor.Interceptor[[_] =>> zio.RIO[gateway.model.auth.SessionContext, _]]

which looks like the same to me!

A simple example would be very, very nice, thanks!

Here’s my attempt at implementing something you might find useful: Tapir + ZIO + interceptor · GitHub

A few remarks:

  1. I made a type alias for RIO[SessionContext, A], which is an alias for ZIO[SessionContext, Throwable, A]. This is the expected effect type in the interceptor, forced by ZioHttpServerOptions[SessionContext].

  2. Since it’s a RIO, we need to handle errors within the interceptors, that’s why I used catchSome to convert a PermissionError into a ServerResponse with status code 401 unauthorized. I’m not fluent with ZIO error handling, so maybe there’s a more idiomatic way, especially that catchSome still preserves error type and I had to add .orDie in the end anyway.

  3. I’m still not sure how SessionContext is going to be provided? In my mock example it’s on the HttpApp.provide() level, but this looks suspicious - don’t we need to somehow resolve it for every request? Or maybe the ZLayer which provides SessionContext will take care of that?

  4. This is probably obvious, but just to make sure: to add an attribute to an endpoint, use
    .attribute(AttributeKey[Permission], Permission("can_list_books")) in endpoint definition.

Thank you! I’ll take a look at it later today, I did make some progress yesterday, mine compiles, but I’ll have to see if it runs.
I like to have separation of concerns:

  • The session context gets provided much earlier in the process, as a middleware, so it looks like (AllApiRoutes.routes @@ attachSessionContext)
  • I was hoping to let the error (zio.fail(PermissionError)) bubble up to my global error handle ,that converts different types of errors to different response objects, I hadn’t thought of letting tAPIr take care of errors, but it may make sense, I’ll have to think about it.
    I have hundreds of endpoints to implement, I want my business logic as tight as possible, easy to understand and unencumbered by permissions, errors handling, etc. or at least as much as possible.

Thanks again!
Roberto

Would anyone happen to know how to make the attributes show up anywhere in the openapi.json when calling the OpenAPIDocsInterpreter?

Would anyone happen to know how to make the attributes show up anywhere in the openapi.json when calling the OpenAPIDocsInterpreter?

You can add documentation extensions using a dedicated openapi attribute, if that’s what you’re after: Generating OpenAPI documentation — tapir 1.x documentation