Skip to content

Commit aad3224

Browse files
committed
Patch IncomingMessage.url in mounted apps
Previously they would get the full request URL. The request URL is now preserved in `originalURL`, and a mounted app receives relative URLs. E.g. sth mounted at "/admin" would get "/view" for "/admin/view". Matches Express.js.
1 parent ec83738 commit aad3224

File tree

5 files changed

+76
-63
lines changed

5 files changed

+76
-63
lines changed

Package.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ let package = Package(
1818
],
1919

2020
dependencies: [
21-
.package(url: "https://github.com/Macro-swift/Macro.git",
22-
from: "1.0.16"),
21+
.package(url: "https://github.com/Macro-swift/Macro.git", from: "1.0.20"),
2322
.package(url: "https://github.com/AlwaysRightInstitute/mustache.git",
2423
from: "1.0.2")
2524
],

Sources/express/Express.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,11 @@ enum ExpressExtKey {
337337
static let loggingKey = "baseurl"
338338
}
339339

340+
enum OriginalURL: EnvironmentKey {
341+
static let defaultValue : String? = nil
342+
static let loggingKey = "originalurl"
343+
}
344+
340345
/**
341346
* Request parameters.
342347
*

Sources/express/IncomingMessage.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ public extension IncomingMessage {
9292
guard let qIdx = url.firstIndex(of: "?") else {
9393
return url.isEmpty ? "/" : url
9494
}
95-
let p = String(url[..<qIdx])
96-
return p.isEmpty ? "/" : p
95+
if url.startIndex == qIdx { return "/" } // empty
96+
return String(url[..<qIdx])
9797
}
9898

9999
/// A reference to the active application. Updated when subapps are triggered.
@@ -144,13 +144,26 @@ public extension IncomingMessage {
144144
}
145145

146146
/**
147-
* Contains the part of the URL which matched the current route. Example:
147+
* The original URL as received from the client, before
148+
* any URL rewriting by mounted middleware.
149+
*
150+
* Unlike ``url``, this is never modified by routing.
151+
* Falls back to ``url`` if not yet set by Express.
152+
*/
153+
var originalURL : String {
154+
get { environment[ExpressExtKey.OriginalURL.self] ?? url }
155+
set { environment[ExpressExtKey.OriginalURL.self] = newValue }
156+
}
157+
158+
/**
159+
* Contains the part of the URL which matched the current
160+
* route. Example:
148161
* ```
149162
* app.get("/admin/index") { ... }
150163
* ```
151164
*
152-
* when this is invoked with "/admin/index/hello/world", the baseURL will
153-
* be "/admin/index".
165+
* when this is invoked with "/admin/index/hello/world",
166+
* the baseURL will be "/admin/index".
154167
*/
155168
var baseURL : String? {
156169
set { environment[ExpressExtKey.BaseURL.self] = newValue }

Sources/express/Route.swift

Lines changed: 42 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,12 @@ open class Route: MiddlewareObject, ErrorMiddlewareObject, RouteKeeper,
157157
{
158158
let ids = debug ? logPrefix : ""
159159
if debug { console.log("\(ids) > enter route:", self) }
160-
160+
161+
// Capture the original URL before any rewriting
162+
if req.environment[ExpressExtKey.OriginalURL.self] == nil {
163+
req.environment[ExpressExtKey.OriginalURL.self] = req.url
164+
}
165+
161166
if let methods = self.methods, !methods.isEmpty {
162167
guard case .request(let head) = req.head,
163168
methods.contains(head.method) else {
@@ -169,59 +174,35 @@ open class Route: MiddlewareObject, ErrorMiddlewareObject, RouteKeeper,
169174
}
170175
}
171176

172-
// FIXME: Could also be a full URL! (CONNECT)
177+
// TBD: Could also be a full URL! (CONNECT)
178+
// req.url is relative to the current mount point
173179
let reqPath = req.url.isEmpty ? "/" : {
174-
// Strip of query parameters and such. This is the raw URL,
175-
// but we need to match just the path.
176180
let s = req.url
177181
if let idx = s.firstIndex(where: { $0 == "#" || $0 == "?" }) {
178182
return String(s[..<idx])
179183
}
180-
else {
181-
return s
182-
}
184+
else { return s }
183185
}()
184186

185187
let params : IncomingMessage.Params
186188
let matchPath : String?
187-
if let pattern = urlPattern { // this route has a path pattern assigned
188-
var newParams = req.params // TBD
189-
190-
if let base = req.baseURL {
191-
let mountPath = String(reqPath[base.endIndex..<reqPath.endIndex])
192-
let comps = split(urlPath: mountPath)
189+
if let pattern = urlPattern {
190+
var newParams = req.params
191+
let comps = split(urlPath: reqPath)
193192

194-
let mountMatchPath = RoutePattern.match(pattern : pattern,
195-
against : comps,
196-
exact : exact,
197-
variables : &newParams)
198-
guard let match = mountMatchPath else {
199-
if debug {
200-
console.log("\(ids) mount route path does not match, next:", self)
201-
}
202-
if let error = error { throw error }
203-
return upperNext()
193+
guard let mp = RoutePattern.match(pattern: pattern, against: comps,
194+
exact: exact, variables: &newParams)
195+
else {
196+
if debug {
197+
console.log(
198+
"\(ids) route path does not match, next:",
199+
self)
204200
}
205-
206-
matchPath = base + match
207-
}
208-
else {
209-
let comps = split(urlPath: reqPath)
210-
211-
guard let mp = RoutePattern.match(pattern : pattern,
212-
against : comps,
213-
exact : exact,
214-
variables : &newParams)
215-
else {
216-
if debug {
217-
console.log("\(ids) route path does not match, next:", self)
218-
}
219-
if let error = error { throw error }
220-
return upperNext()
221-
}
222-
matchPath = mp
201+
if let error = error { throw error }
202+
return upperNext()
223203
}
224-
204+
matchPath = mp
205+
225206
if debug { console.log("\(ids) path match:", matchPath) }
226207

227208
params = newParams
@@ -235,18 +216,31 @@ open class Route: MiddlewareObject, ErrorMiddlewareObject, RouteKeeper,
235216
guard !self.errorMiddleware.isEmpty else { throw error }
236217
}
237218
else {
238-
guard !self.middleware .isEmpty else { return upperNext() }
219+
guard !self.middleware.isEmpty else { return upperNext() }
239220
}
240221

241222
// push route state
242223
let oldParams = req.params
243224
let oldRoute = req.route
244225
let oldBase = req.baseURL
245-
req.params = params
246-
req.route = self
226+
let oldUrl = req.url
227+
req.params = params
228+
req.route = self
247229
if let mp = matchPath {
248-
req.baseURL = mp
249-
if debug { console.log("\(ids) push baseURL:", req.baseURL) }
230+
req.baseURL = (oldBase ?? "") + mp
231+
if !exact {
232+
// Rewrite req.url like Node.js Express:
233+
// strip the matched prefix, keep query/fragment.
234+
var newUrl = String(req.url.dropFirst(mp.count))
235+
if newUrl.isEmpty { newUrl = "/" }
236+
else if newUrl.first == "?" || newUrl.first == "#" {
237+
newUrl = "/" + newUrl
238+
}
239+
req.url = newUrl
240+
}
241+
if debug {
242+
console.log("\(ids) push baseURL:", req.baseURL, "url:", req.url)
243+
}
250244
}
251245

252246
final class MiddlewareWalker {
@@ -340,6 +334,7 @@ open class Route: MiddlewareObject, ErrorMiddlewareObject, RouteKeeper,
340334
req.params = oldParams
341335
req.route = oldRoute
342336
req.baseURL = oldBase
337+
req.url = oldUrl
343338

344339
if let arg = args.first { // lame 1-object spread to pass on errors
345340
upperNext(arg)

Tests/RouteTests/RouteMountingTests.swift

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import XCTest
22
import MacroTestUtilities
3-
import class http.IncomingMessage
3+
import http // IncomingMessage
44
@testable import express
55

66
final class RouteMountingTests: XCTestCase {
@@ -13,15 +13,15 @@ final class RouteMountingTests: XCTestCase {
1313
outerRoute.route("/admin")
1414
.get("/view") { req, res, next in
1515
XCTAssertEqual(req.baseURL, "/admin/view", "baseURL does not match")
16-
XCTAssertEqual(req.url, "/admin/view", "HTTP URL does not match")
16+
XCTAssertEqual(req.url, "/view", "url should be relative to mount")
17+
XCTAssertEqual(req.originalURL, "/admin/view",
18+
"originalURL should be the full path")
1719
didCallRoute = true
1820
}
1921

2022
// test
2123

22-
let req = IncomingMessage(
23-
.init(version: .init(major: 1, minor: 1), method: .GET,
24-
uri: "/admin/view"))
24+
let req = IncomingMessage(url: "/admin/view")
2525
let res = TestServerResponse()
2626

2727
var didCallNext = false
@@ -46,7 +46,10 @@ final class RouteMountingTests: XCTestCase {
4646
outerRoute.route(id: "outer", "/admin")
4747
.get(id: "error", "/view") { error, req, res, next in
4848
XCTAssertEqual(req.baseURL, "/admin/view", "baseURL does not match")
49-
XCTAssertEqual(req.url, "/admin/view", "HTTP URL does not match")
49+
XCTAssertEqual(req.url, "/view?answer=42",
50+
"url should be relative to mount")
51+
XCTAssertEqual(req.originalURL, "/admin/view?answer=42",
52+
"originalURL should be the full path")
5053
didCallErrorMiddleware = true
5154
}
5255
.use(id: "thrower") { req, res, next in
@@ -56,9 +59,7 @@ final class RouteMountingTests: XCTestCase {
5659

5760
// test
5861

59-
let req = IncomingMessage(
60-
.init(version: .init(major: 1, minor: 1), method: .GET,
61-
uri: "/admin/view"))
62+
let req = IncomingMessage(url: "/admin/view?answer=42"))
6263
let res = TestServerResponse()
6364

6465
var didCallNext = false

0 commit comments

Comments
 (0)