softwaremill / tapir

Rapid development of self-documenting APIs
https://tapir.softwaremill.com
Apache License 2.0
1.35k stars 406 forks source link

[BUG] ZioHttpInterpreter: Route with literal path segment makes route with variable path segment unavailable #3992

Closed wwbakker closed 9 hours ago

wwbakker commented 4 weeks ago

Tapir version: Works in tapir 1.10.10 and before, broken since tapir 1.10.11 and later

Describe the bug

Consider the following code:

//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server::1.10.10

import zio.*
import zio.http.*
import sttp.tapir.generic.auto.*
import sttp.tapir.ztapir.*
import sttp.tapir.*
import sttp.tapir.server.ziohttp.ZioHttpInterpreter

object TestServer extends ZIOAppDefault with Tapir:

  val ep1 = endpoint.get.in("base" / "type1" / "fixed").out(stringBody)
  val ep2 = endpoint.get.in("base" / path[String]("type") / "dynamic").out(stringBody)
  val app = ZioHttpInterpreter()
    .toHttp(
      List(
        ep1.zServerLogic(_ => ZIO.succeed("Fixed endpoint - tapir 1.10.10")),
        ep2.zServerLogic(_ => ZIO.succeed("Dynamic endpoint - tapir 1.10.10"))
      )
    )

  override def run =
    Server
      .serve(app)
        .provideSomeLayer[Scope](
          ZLayer.succeed(Server.Config.default.port(8060)) >>> Server.live
        )

If I call the endpoints, both work as expected:

GET http://localhost:8060/base/type1/fixed

HTTP/1.1 200 OK
content-length: 30
Content-Type: text/plain; charset=UTF-8
date: Mon, 19 Aug 2024 13:47:23 GMT

Fixed endpoint - tapir 1.10.10

Response code: 200 (OK); Time: 231ms (231 ms); Content length: 30 bytes (30 B)
GET http://localhost:8060/base/type1/dynamic

HTTP/1.1 200 OK
content-length: 32
Content-Type: text/plain; charset=UTF-8
date: Mon, 19 Aug 2024 13:47:46 GMT

Dynamic endpoint - tapir 1.10.10

Response code: 200 (OK); Time: 16ms (16 ms); Content length: 32 bytes (32 B)

If I do the same with a newer version (same code, just versions changed):

//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server::1.11.1

import zio.*
import zio.http.*
import sttp.tapir.generic.auto.*
import sttp.tapir.ztapir.*
import sttp.tapir.*
import sttp.tapir.server.ziohttp.ZioHttpInterpreter

object TestServer extends ZIOAppDefault with Tapir:

  val ep1 = endpoint.get.in("base" / "type1" / "fixed").out(stringBody)
  val ep2 = endpoint.get.in("base" / path[String]("type") / "dynamic").out(stringBody)
  val app = ZioHttpInterpreter()
    .toHttp(
      List(
        ep1.zServerLogic(_ => ZIO.succeed("Fixed endpoint - tapir 1.11.1")),
        ep2.zServerLogic(_ => ZIO.succeed("Dynamic endpoint - tapir 1.11.1"))
      )
    )

  override def run =
    Server
      .serve(app)
        .provideSomeLayer[Scope](
          ZLayer.succeed(Server.Config.default.port(8060)) >>> Server.live
        )

If I call the endpoints, only the first one works:

GET http://localhost:8060/base/type1/fixed

HTTP/1.1 200 OK
content-length: 29
Content-Type: text/plain; charset=UTF-8
date: Mon, 19 Aug 2024 13:49:31 GMT

Fixed endpoint - tapir 1.11.1

Response code: 200 (OK); Time: 209ms (209 ms); Content length: 29 bytes (29 B)
GET http://localhost:8060/base/type1/dynamic

HTTP/1.1 404 Not Found
warning: 199 ZIO HTTP "/base/type1/dynamic"
date: Mon, 19 Aug 2024 13:50:01 GMT
content-length: 0

<Response body is empty>

Response code: 404 (Not Found); Time: 11ms (11 ms); Content length: 0 bytes (0 B)

Additional information I also did some research into whether this might be a zio-http issue. I see however that when using tapir, the routing is done by ZioHttpInterpreter, instead of the routing code from zio-http itself.

The interesting/funny thing there is that zio-http has the same bug, but as far as I can tell it has never worked there. (https://github.com/zio/zio-http/issues/3036)

My best guess is that the issue was introduced in this PR: https://github.com/softwaremill/tapir/pull/3856

felix-hedenstrom commented 5 days ago

We've also observed this issue and it prevents us from upgrading beyond 1.10.10.

felix-hedenstrom commented 15 hours ago

I ran your example with 1.11.3 and it works now. Must have been fixed at some point.

wwbakker commented 14 hours ago

That's good news. Seems to be fixed specifically in 1.11.3, because 1.11.2 still fails. Possibly the 3.0.0 zio-http support?

I will check in a real project hopefully later today, and if that fixes it I'll close the ticket.

adamw commented 14 hours ago

I added a test, passes (at least locally). Let me know if there would be any more problems

wwbakker commented 9 hours ago

Works well now, thanks @adamw @felix-hedenstrom and the contributors to the 1.11.3!