Skip to content

Commit 0edbc3c

Browse files
authored
In Pekko/Akka, ServerRequest.uri should return the full path (#4973)
Closes #4972
1 parent c055792 commit 0edbc3c

File tree

4 files changed

+75
-11
lines changed

4 files changed

+75
-11
lines changed

server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/AkkaServerRequest.scala

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ private[akkahttp] case class AkkaServerRequest(ctx: RequestContext, attributes:
1616
override lazy val connectionInfo: ConnectionInfo = ConnectionInfo(None, None, None)
1717
override def underlying: Any = ctx
1818

19-
override lazy val pathSegments: List[String] = {
19+
private def akkaPathToSegments(p: AkkaUri.Path): List[String] = {
2020
@tailrec
2121
def run(p: AkkaUri.Path, acc: List[String]): List[String] = p match {
2222
case AkkaUri.Path.Slash(pathTail) => run(pathTail, acc)
2323
case AkkaUri.Path.Segment(s, pathTail) => run(pathTail, s :: acc)
2424
case _ => acc.reverse
2525
}
26-
27-
run(ctx.unmatchedPath, Nil)
26+
run(p, Nil)
2827
}
28+
29+
override lazy val pathSegments: List[String] = akkaPathToSegments(ctx.unmatchedPath)
30+
2931
override lazy val queryParameters: QueryParams = QueryParams.fromMultiMap(ctx.request.uri.query().toMultiMap)
3032
override lazy val method: Method = Method(ctx.request.method.value.toUpperCase)
3133

@@ -46,13 +48,16 @@ private[akkahttp] case class AkkaServerRequest(ctx: RequestContext, attributes:
4648
}
4749

4850
override lazy val showShort: String = s"$method ${ctx.request.uri.path}${ctx.request.uri.rawQueryString.getOrElse("")}"
51+
4952
override lazy val uri: Uri = {
50-
val pekkoUri = ctx.request.uri
53+
val akkaUri = ctx.request.uri
54+
// Use the full path from the original request, not the unconsumed path
55+
val fullPathSegments = akkaPathToSegments(akkaUri.path)
5156
Uri(
52-
Some(pekkoUri.scheme),
57+
Some(akkaUri.scheme),
5358
// UserInfo is available only as a raw string, but we can skip it as it's not needed
54-
Some(Authority(userInfo = None, HostSegment(pekkoUri.authority.host.address), Some(pekkoUri.effectivePort))),
55-
PathSegments.absoluteOrEmptyS(pathSegments ++ (if (pekkoUri.path.endsWithSlash) Seq("") else Nil)),
59+
Some(Authority(userInfo = None, HostSegment(akkaUri.authority.host.address), Some(akkaUri.effectivePort))),
60+
PathSegments.absoluteOrEmptyS(fullPathSegments ++ (if (akkaUri.path.endsWithSlash) Seq("") else Nil)),
5661
queryToSegments(ctx.request.uri.query()),
5762
ctx.request.uri.fragment.map(f => FragmentSegment(f))
5863
)

server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpServerTest.scala

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,33 @@ class AkkaHttpServerTest extends TestSuite with EitherValues {
202202
}
203203
}
204204
.unsafeToFuture()
205+
},
206+
Test("extractFromRequest(_.uri) returns full URI when nested in path directive") {
207+
// Given: an endpoint that extracts the URI from the request
208+
val e = endpoint.get
209+
.in("test" / "path")
210+
.in(extractFromRequest(_.uri))
211+
.out(stringBody)
212+
.serverLogic { requestUri =>
213+
requestUri.toString.asRight[Unit].unit
214+
}
215+
216+
// When: the route is nested inside a pathPrefix directive
217+
val route = Directives.pathPrefix("api")(AkkaHttpServerInterpreter().toRoute(e))
218+
219+
interpreter
220+
.server(route)
221+
.use { port =>
222+
// Then: the extracted URI should contain the full path including the prefix
223+
basicRequest
224+
.get(uri"http://localhost:$port/api/test/path?query=value")
225+
.send(backend)
226+
.map { response =>
227+
response.body.value should include("/api/test/path")
228+
response.body.value should include("query=value")
229+
}
230+
}
231+
.unsafeToFuture()
205232
}
206233
)
207234
def drainAkka(stream: AkkaStreams.BinaryStream): Future[Unit] =

server/pekko-http-server/src/main/scala/sttp/tapir/server/pekkohttp/PekkoServerRequest.scala

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,19 @@ private[pekkohttp] case class PekkoServerRequest(ctx: RequestContext, attributes
2929
}
3030
override def underlying: Any = ctx
3131

32-
override lazy val pathSegments: List[String] = {
32+
/** Converts a Pekko Uri.Path to a list of path segment strings. */
33+
private def pekkoPathToSegments(p: PekkoUri.Path): List[String] = {
3334
@tailrec
3435
def run(p: PekkoUri.Path, acc: List[String]): List[String] = p match {
3536
case PekkoUri.Path.Slash(pathTail) => run(pathTail, acc)
3637
case PekkoUri.Path.Segment(s, pathTail) => run(pathTail, s :: acc)
3738
case _ => acc.reverse
3839
}
39-
40-
run(ctx.unmatchedPath, Nil)
40+
run(p, Nil)
4141
}
42+
43+
override lazy val pathSegments: List[String] = pekkoPathToSegments(ctx.unmatchedPath)
44+
4245
override lazy val queryParameters: QueryParams = QueryParams.fromMultiMap(ctx.request.uri.query().toMultiMap)
4346
override lazy val method: Method = Method(ctx.request.method.value.toUpperCase)
4447

@@ -61,11 +64,13 @@ private[pekkohttp] case class PekkoServerRequest(ctx: RequestContext, attributes
6164
override lazy val showShort: String = s"$method ${ctx.request.uri.path}${ctx.request.uri.rawQueryString.getOrElse("")}"
6265
override lazy val uri: Uri = {
6366
val pekkoUri = ctx.request.uri
67+
// Use the full path from the original request, not the unconsumed path
68+
val fullPathSegments = pekkoPathToSegments(pekkoUri.path)
6469
Uri(
6570
Some(pekkoUri.scheme),
6671
// UserInfo is available only as a raw string, but we can skip it as it's not needed
6772
Some(Authority(userInfo = None, HostSegment(pekkoUri.authority.host.address), Some(pekkoUri.effectivePort))),
68-
PathSegments.absoluteOrEmptyS(pathSegments ++ (if (pekkoUri.path.endsWithSlash) Seq("") else Nil)),
73+
PathSegments.absoluteOrEmptyS(fullPathSegments ++ (if (pekkoUri.path.endsWithSlash) Seq("") else Nil)),
6974
queryToSegments(ctx.request.uri.query()),
7075
ctx.request.uri.fragment.map(f => FragmentSegment(f))
7176
)

server/pekko-http-server/src/test/scala/sttp/tapir/server/pekkohttp/PekkoHttpServerTest.scala

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,33 @@ class PekkoHttpServerTest extends TestSuite with EitherValues {
183183
.map(_.body.value should fullyMatch regex """Some\(/127\.0\.0\.1:\d+\) Some\(false\)""")
184184
}
185185
.unsafeToFuture()
186+
},
187+
Test("extractFromRequest(_.uri) returns full URI when nested in path directive") {
188+
// Given: an endpoint that extracts the URI from the request
189+
val e = endpoint.get
190+
.in("test" / "path")
191+
.in(extractFromRequest(_.uri))
192+
.out(stringBody)
193+
.serverLogic { requestUri =>
194+
requestUri.toString.asRight[Unit].unit
195+
}
196+
197+
// When: the route is nested inside a pathPrefix directive
198+
val route = Directives.pathPrefix("api")(PekkoHttpServerInterpreter().toRoute(e))
199+
200+
interpreter
201+
.server(route)
202+
.use { port =>
203+
// Then: the extracted URI should contain the full path including the prefix
204+
basicRequest
205+
.get(uri"http://localhost:$port/api/test/path?query=value")
206+
.send(backend)
207+
.map { response =>
208+
response.body.value should include("/api/test/path")
209+
response.body.value should include("query=value")
210+
}
211+
}
212+
.unsafeToFuture()
186213
}
187214
)
188215
def drainPekko(stream: PekkoStreams.BinaryStream): Future[Unit] =

0 commit comments

Comments
 (0)