tapir icon indicating copy to clipboard operation
tapir copied to clipboard

Odd fragility in Tapir/zio-http example

Open swaldman opened this issue 3 years ago • 7 comments

Tapir version: 1.2.6

Scala version: 3.2.1

I'm new to both Tapir and ZIO. Please forgive me if I'm missing something obvious!

I've been working on an application using HelloWorldZioHttpServer as an inspiration. I noticed a peculiar fragility. Things are fine if I define my HttpApp in a single expression like

 val app0 =                                                                                                                                                                     
    ZioHttpInterpreter().toHttp( helloWorld.zServerLogic(name => ZIO.succeed(s"Hello, $name!") ) )                                                                               

But the application fails to compile, demanding I provide a ZLayer for scala.Nothing, if I break up the long expression into what I think should produce an identical result:

 val app1 =                                                                                                                                                                     
    val se = helloWorld.zServerLogic(name => ZIO.succeed(s"Hello, $name!") )                                                                                                     
    ZioHttpInterpreter().toHttp( se )                                                                                                                                            

How to reproduce?

Stick this file alone in a directory, and then scala-cli .

It won't compile. Change app1 to app0 in Server.serve(...), then it will, and it works fine!

//> using scala "3.2.1"
//> using lib "com.softwaremill.sttp.tapir::tapir-zio:1.2.6"
//> using lib "com.softwaremill.sttp.tapir::tapir-zio-http-server:1.2.6"

import sttp.tapir.PublicEndpoint
import sttp.tapir.ztapir.*
import sttp.tapir.server.ziohttp.ZioHttpInterpreter
import zio.http.{Http, HttpApp}
import zio.http.{Server, ServerConfig}
import zio.*

object FragileHelloServer extends ZIOAppDefault {

  val helloWorld: PublicEndpoint[String, Unit, String, Any] =
    endpoint.get
      .in("hello")
      .in(path[String]("name"))
      .out(stringBody)

  val app0 =
    ZioHttpInterpreter().toHttp( helloWorld.zServerLogic(name => ZIO.succeed(s"Hello, $name!") ) )

  val app1 =
    val se = helloWorld.zServerLogic(name => ZIO.succeed(s"Hello, $name!") )
    ZioHttpInterpreter().toHttp( se )

  // starting the server
  override def run =
    val server = Server.serve(app1)
    server.provide(
      ServerConfig.live(ServerConfig.default.port(8999)),
      Server.live,
    ).exitCode
}

Additional information

$ java -version
java version "17.0.5" 2022-10-18 LTS
Java(TM) SE Runtime Environment (build 17.0.5+9-LTS-191)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.5+9-LTS-191, mixed mode, sharing)

swaldman avatar Jan 24 '23 00:01 swaldman

I'm not sure why, but Scala 3 (works fine w/ Scala 2), infers the type of se in app1 as ZServerEndpoint[Nothing, Any] instead of ZServerEndpoint[Any, Any].

A work-around is to explicitly type this: val se: ZServerEndpoint[Any, Any] or explicitly provide Any as the environment type: helloWorld.zServerLogic[Any](...)

adamw avatar Feb 03 '23 19:02 adamw

Hi! Thanks! It wasn't hard to work around, and tapir is great. It was just a curiousity, I'd think whatever type the compiler inferred for se would be the inferred type whether it was passed directly or retained as a val. It's probably more a compiler oddity than a tapir one.

I have found endpoint typing a bit confusing in tapir. I pass them around as ZServerEndpoint[Any, Any] and everything works and I am very happy. But I don't quite understand the meaning of the second type parameter C in ZServerEndpoint[R, C]. If I understand correctly, the parameter R becomes the usual R in ZIO[R,E,A], but the C parameter is also a requirements parameter (R in the non-Z version ofServerEndpoint) whose role I don't quite understand. If there's an easy pointer (emphasis on easy, for you to send along), let me know.

Thank you for taking the time to look into this, and for a quite brilliant library.

swaldman avatar Feb 03 '23 20:02 swaldman

It was just a curiousity, I'd think whatever type the compiler inferred for se would be the inferred type whether it was passed directly or retained as a val. It's probably more a compiler oddity than a tapir one.

Yeah, we might not use the correct variance (currently the parameter is invariant). Sth to experiment with. I'll reopen the issue to investigate this at some time.

I have found endpoint typing a bit confusing in tapir. I pass them around as ZServerEndpoint[Any, Any] and everything works and I am very happy. But I don't quite understand the meaning of the second type parameter C in ZServerEndpoint[R, C]. If I understand correctly, the parameter R becomes the usual R in ZIO[R,E,A], but the C parameter is also a requirements parameter (R in the non-Z version ofServerEndpoint) whose role I don't quite understand. If there's an easy pointer (emphasis on easy, for you to send along), let me know.

The C stands for capabilities: https://github.com/softwaremill/tapir/blob/master/core/src/main/scala/sttp/tapir/server/ServerEndpoint.scala#L12-L13. This might include specific requirements for non-blocking streaming that the endpoint uses, or websockets. These capabilities must be provided by the interpreter. For example, the ZioHttpServerInterpreter supports the ZioStreams capability, as you can use zio streams in your endpint. Such an endpoint would have the type ZServerEndpoint[Any, ZioStreams]

adamw avatar Feb 03 '23 20:02 adamw

Thank you!

swaldman avatar Feb 03 '23 23:02 swaldman

This seems to be the same problem: https://softwaremill.community/t/unit-testing-with-scala-3-sttp-tapir-zio/213/2

adamw avatar Jun 09 '23 08:06 adamw

(no solution, though :) )

adamw avatar Jun 09 '23 08:06 adamw

(hasn't been hard to work around! :)

swaldman avatar Jun 09 '23 09:06 swaldman