Skip to content

Commit 3082fae

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

File tree

8 files changed

+91
-28
lines changed

8 files changed

+91
-28
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

zio-http-testkit/src/main/scala/zio/http/TestClient.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ final case class TestClient(
7272
r <- ZIO.environment[R]
7373
provided = route.provideEnvironment(r)
7474
_ <- behavior.update(_ :+ provided)
75+
_ <- behavior.get.debug("Added route")
7576
} yield ()
7677

7778
/**
@@ -121,7 +122,7 @@ final case class TestClient(
121122
proxy: Option[Proxy],
122123
)(implicit trace: Trace): ZIO[Any, Throwable, Response] = {
123124
for {
124-
currentBehavior <- behavior.get.map(_ :+ Method.ANY / trailing -> handler(Response.notFound))
125+
currentBehavior <- behavior.get
125126
request = Request(
126127
body = body,
127128
headers = headers,
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-testkit/src/test/scala/zio/http/TestClientSpec.scala

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ object TestClientSpec extends ZIOHttpSpec {
2222
_ <- TestClient.addRequestResponse(request2, Response.ok)
2323
goodResponse2 <- client(request)
2424
badResponse2 <- client(request2)
25-
} yield assertTrue(extractStatus(goodResponse) == Status.Ok) && assertTrue(
25+
} yield assertTrue(
26+
extractStatus(goodResponse) == Status.Ok,
2627
extractStatus(badResponse) == Status.NotFound,
27-
) &&
28-
assertTrue(extractStatus(goodResponse2) == Status.Ok) && assertTrue(
29-
extractStatus(badResponse2) == Status.Ok,
30-
)
28+
extractStatus(goodResponse2) == Status.Ok,
29+
extractStatus(badResponse2) == Status.Ok,
30+
)
3131
},
3232
),
3333
suite("addHandler")(

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ object RoutePatternSpec extends ZIOHttpSpec {
249249
tree = tree.add(pattern2, 2)
250250
tree = tree.add(pattern3, 3)
251251

252-
assertTrue(tree.get(Method.OPTIONS, Path("/users")) == Chunk(2, 1, 3))
252+
assertTrue(tree.get(Method.OPTIONS, Path("/users")) == Chunk(2))
253253
},
254254
test("multiple routes") {
255255
var tree: Tree[Unit] = RoutePattern.Tree.empty

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/RoutePattern.scala

+1-4
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,7 @@ object RoutePattern {
216216
}
217217
}
218218

219-
if (anyRoot eq null) forMethod
220-
else {
221-
forMethod ++ anyRoot.get(path)
222-
}
219+
if (forMethod.isEmpty && anyRoot != null) anyRoot.get(path) else forMethod
223220
}
224221

225222
def map[B](f: A => B): Tree[B] =

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)