Skip to content

Commit 71cf65f

Browse files
committed
Make newly added Routes take precedence over old ones (zio#3066)
1 parent 08a2531 commit 71cf65f

File tree

4 files changed

+82
-17
lines changed

4 files changed

+82
-17
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ jobs:
603603
matrix:
604604
os: [ubuntu-latest]
605605
scala: [2.13.16]
606-
java: [temurin@8]
606+
java: [zulu@8]
607607
runs-on: ${{ matrix.os }}
608608
steps:
609609
- uses: coursier/setup-action@v1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package zio.http
2+
3+
import zio._
4+
import zio.test.TestAspect.shrinks
5+
import zio.test._
6+
7+
import zio.http.endpoint.{AuthType, Endpoint}
8+
import zio.http.netty.NettyConfig
9+
import zio.http.netty.server.NettyDriver
10+
11+
object RoutesPrecedentsSpec extends ZIOSpecDefault {
12+
13+
trait MyService {
14+
def code: UIO[Int]
15+
}
16+
object MyService {
17+
def live(code: Int): ULayer[MyService] = ZLayer.succeed(new MyServiceLive(code))
18+
}
19+
final class MyServiceLive(_code: Int) extends MyService {
20+
def code: UIO[Int] = ZIO.succeed(_code)
21+
}
22+
23+
val endpoint: Endpoint[Unit, String, ZNothing, Int, AuthType.None] =
24+
Endpoint(RoutePattern.POST / "api").in[String].out[Int]
25+
26+
val api = endpoint.implement(_ => ZIO.serviceWithZIO[MyService](_.code))
27+
28+
// when adding the same route multiple times to the server, the last one should take precedence
29+
override def spec: Spec[TestEnvironment & Scope, Any] =
30+
test("test") {
31+
check(Gen.fromIterable(List(1, 2, 3, 4, 5))) { code =>
32+
(
33+
for {
34+
client <- ZIO.service[Client]
35+
port <- ZIO.serviceWithZIO[Server](_.port)
36+
url = URL.root.port(port) / "api"
37+
request = Request
38+
.post(url = url, body = Body.fromString(""""this is some input""""))
39+
.addHeader(Header.Accept(MediaType.application.json))
40+
_ <- TestServer.addRoutes(api.toRoutes)
41+
result <- client.batched(request)
42+
output <- result.body.asString
43+
} yield assertTrue(output == code.toString)
44+
).provideSome[TestServer & Client](
45+
ZLayer.succeed(new MyServiceLive(code)),
46+
)
47+
}.provide(
48+
ZLayer.succeed(Server.Config.default.onAnyOpenPort),
49+
TestServer.layer,
50+
Client.default,
51+
NettyDriver.customized,
52+
ZLayer.succeed(NettyConfig.defaultWithFastShutdown),
53+
)
54+
} @@ shrinks(0)
55+
}

zio-http/jvm/src/test/scala/zio/http/RoutesSpec.scala

+23-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package zio.http
1818

1919
import zio.test._
2020

21-
import zio.http.codec.PathCodec
21+
import zio.http.codec.{PathCodec, SegmentCodec}
2222

2323
object RoutesSpec extends ZIOHttpSpec {
2424
def extractStatus(response: Response): Status = response.status
@@ -108,5 +108,27 @@ object RoutesSpec extends ZIOHttpSpec {
108108
)
109109
}
110110
},
111+
test("overlapping routes with different segment types") {
112+
val app = Routes(
113+
Method.GET / "foo" / string("id") -> Handler.status(Status.NoContent),
114+
Method.GET / "foo" / string("id") -> Handler.ok,
115+
Method.GET / "foo" / (SegmentCodec.literal("prefix") ~ string("rest")) -> Handler.ok,
116+
Method.GET / "foo" / int("id") -> Handler.ok,
117+
)
118+
119+
for {
120+
stringId <- app.runZIO(Request.get("/foo/123"))
121+
stringPrefix <- app.runZIO(Request.get("/foo/prefix123"))
122+
intId <- app.runZIO(Request.get("/foo/123"))
123+
notFound <- app.runZIO(Request.get("/foo/123/456"))
124+
} yield {
125+
assertTrue(
126+
extractStatus(stringId) == Status.Ok,
127+
extractStatus(stringPrefix) == Status.Ok,
128+
extractStatus(intId) == Status.Ok,
129+
extractStatus(notFound) == Status.NotFound,
130+
)
131+
}
132+
},
111133
)
112134
}

zio-http/shared/src/main/scala/zio/http/Routes.scala

+3-15
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,8 @@ final case class Routes[-Env, +Err](routes: Chunk[zio.http.Route[Env, Err]]) { s
4848
new ApplyContextAspect[Env, Err, Env0](self)
4949

5050
/**
51-
* Combines this HTTP application with the specified HTTP application. In case
52-
* of route conflicts, the routes in this HTTP application take precedence
53-
* over the routes in the specified HTTP application.
51+
* Combines this Routes with the specified Routes. In case of route conflicts,
52+
* the new Routes take precedence over the current Routes.
5453
*/
5554
def ++[Env1 <: Env, Err1 >: Err](that: Routes[Env1, Err1]): Routes[Env1, Err1] =
5655
copy(routes = routes ++ that.routes)
@@ -252,18 +251,7 @@ final case class Routes[-Env, +Err](routes: Chunk[zio.http.Route[Env, Err]]) { s
252251
chunk.length match {
253252
case 0 => Handler.notFound
254253
case 1 => chunk(0)
255-
case n => // TODO: Support precomputed fallback among all chunk elements
256-
var acc = chunk(0)
257-
var i = 1
258-
while (i < n) {
259-
val h = chunk(i)
260-
acc = acc.catchAll { response =>
261-
if (response.status == Status.NotFound) h
262-
else Handler.fail(response)
263-
}
264-
i += 1
265-
}
266-
acc
254+
case _ => chunk.last
267255
}
268256
}
269257
.merge

0 commit comments

Comments
 (0)