Skip to content

Commit 342a757

Browse files
authored
feat: expose sanitizeUrlPath (#415)
1 parent 6c01026 commit 342a757

File tree

4 files changed

+82
-0
lines changed

4 files changed

+82
-0
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Do you need a real-world example that uses this router? Check out [Fastify](http
2828
- [hasRoute (method, path, [constraints])](#hasroute-method-path-constraints)
2929
- [lookup(request, response, [context], [done])](#lookuprequest-response-context-done)
3030
- [find(method, path, [constraints])](#findmethod-path-constraints)
31+
- [sanitizeUrlPath(url, [useSemicolonDelimiter])](#sanitizeurlpathurl-usesemicolondelimiter)
3132
- [prettyPrint([{ method: 'GET', commonPrefix: false, includeMeta: true || [] }])](#prettyprint-commonprefix-false-includemeta-true---)
3233
- [reset()](#reset)
3334
- [routes](#routes)
@@ -489,6 +490,27 @@ router.find('GET', '/example', { host: 'fastify.io', version: '1.x' })
489490
// => null
490491
```
491492

493+
#### sanitizeUrlPath(url, [useSemicolonDelimiter])
494+
Sanitize and decode a URL path using the same logic that `lookup` uses internally.
495+
496+
```js
497+
const FindMyWay = require('find-my-way')
498+
499+
const url = '/foo%20bar?foo=bar'
500+
const path = FindMyWay.sanitizeUrlPath(url)
501+
502+
console.log(path) // '/foo bar'
503+
```
504+
505+
If you need to support `;` as a query string delimiter (for example `/foo;bar=1`), pass `useSemicolonDelimiter: true`:
506+
507+
```js
508+
const path = FindMyWay.sanitizeUrlPath('/foo;bar=1', true)
509+
console.log(path) // '/foo'
510+
```
511+
512+
This function will throw an error if the URL is malformed.
513+
492514
<a name="pretty-print"></a>
493515
#### prettyPrint([{ commonPrefix: false, includeMeta: true || [] }])
494516
`find-my-way` builds a tree of routes for each HTTP method. If you call the `prettyPrint`

index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,14 @@ Router.prototype.all = function (path, handler, store) {
762762
this.on(httpMethods, path, handler, store)
763763
}
764764

765+
Router.sanitizeUrlPath = function sanitizeUrlPath (url, useSemicolonDelimiter) {
766+
const decoded = safeDecodeURI(url, useSemicolonDelimiter)
767+
if (decoded.shouldDecodeParam) {
768+
return safeDecodeURIComponent(decoded.path)
769+
}
770+
return decoded.path
771+
}
772+
765773
module.exports = Router
766774

767775
function escapeRegExp (string) {

lib/url-sanitizer.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ function decodeComponentChar (highCharCode, lowCharCode) {
3434
return null
3535
}
3636

37+
/**
38+
* Safely decodes a URI path, preserving reserved characters in querystring.
39+
*
40+
* @param {string} path - The full request path, possibly including querystring.
41+
* @param {boolean} [useSemicolonDelimiter] - When true, also treat `;` as a query delimiter.
42+
* @returns {{ path: string, querystring: string, shouldDecodeParam: boolean }}
43+
* An object containing the decoded path, the raw querystring, and a flag indicating
44+
* whether any path parameters contain percent-encoded reserved characters.
45+
*/
3746
function safeDecodeURI (path, useSemicolonDelimiter) {
3847
let shouldDecode = false
3948
let shouldDecodeParam = false

test/url-sanitizer.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict'
2+
3+
const { test } = require('node:test')
4+
const FindMyWay = require('..')
5+
6+
test('sanitizeUrlPath should decode reserved characters inside params and strip querystring', t => {
7+
t.plan(1)
8+
9+
const url = '/%65ncod%65d?foo=bar'
10+
const sanitized = FindMyWay.sanitizeUrlPath(url)
11+
12+
t.assert.equal(sanitized, '/encoded')
13+
})
14+
15+
test('sanitizeUrlPath should decode non-reserved characters but keep reserved encoded when not in params', t => {
16+
t.plan(1)
17+
18+
const url = '/hello/%20world?foo=bar'
19+
const sanitized = FindMyWay.sanitizeUrlPath(url)
20+
21+
t.assert.equal(sanitized, '/hello/ world')
22+
})
23+
24+
test('sanitizeUrlPath should treat semicolon as queryparameter delimiter when enabled', t => {
25+
t.plan(2)
26+
27+
const url = '/hello/%23world;foo=bar'
28+
29+
const sanitizedWithDelimiter = FindMyWay.sanitizeUrlPath(url, true)
30+
t.assert.equal(sanitizedWithDelimiter, '/hello/#world')
31+
32+
const sanitizedWithoutDelimiter = FindMyWay.sanitizeUrlPath(url, false)
33+
t.assert.equal(sanitizedWithoutDelimiter, '/hello/#world;foo=bar')
34+
})
35+
36+
test('sanitizeUrlPath trigger an error if the url is invalid', t => {
37+
t.plan(1)
38+
39+
const url = '/Hello%3xWorld/world'
40+
t.assert.throws(() => {
41+
FindMyWay.sanitizeUrlPath(url)
42+
}, 'URIError: URI malformed')
43+
})

0 commit comments

Comments
 (0)