Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add querrystring parser #284

Merged
merged 1 commit into from
May 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ const router = require('find-my-way')({
})
```

The default query string parser that find-my-way uses is the Node.js's core querystring module. You can change this default setting by passing the option querystringParser and use a custom one, such as [qs](https://www.npmjs.com/package/qs).

```js
const qs = require('qs')
const router = require('find-my-way')({
querystringParser: str => qs.parse(str)
})

router.on('GET', '/', (req, res, params, store, searchParams) => {
assert.equal(searchParams, { foo: 'bar', baz: 'faz' })
})

router.lookup({ method: 'GET', url: '/?foo=bar&baz=faz' }, null)
```

You can assign a `buildPrettyMeta` function to sanitize a route's `store` object to use with the `prettyPrint` functions. This function should accept a single object and return an object.

```js
Expand Down Expand Up @@ -227,7 +242,7 @@ The signature of the functions and objects must match the one from the example a
#### on(method, path, [opts], handler, [store])
Register a new route.
```js
router.on('GET', '/example', (req, res, params) => {
router.on('GET', '/example', (req, res, params, store, searchParams) => {
// your code
})
```
Expand Down
4 changes: 3 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ declare namespace Router {
req: Req<V>,
res: Res<V>,
params: { [k: string]: string | undefined },
store: any
store: any,
searchParams: { [k: string]: string }
) => any;

interface ConstraintStrategy<V extends HTTPVersion, T = string> {
Expand Down Expand Up @@ -110,6 +111,7 @@ declare namespace Router {
handler: Handler<V>;
params: { [k: string]: string | undefined };
store: any;
searchParams: { [k: string]: string };
}

interface Instance<V extends HTTPVersion> {
Expand Down
22 changes: 18 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

const assert = require('assert')
const http = require('http')
const querystring = require('querystring')
const isRegexSafe = require('safe-regex2')
const deepEqual = require('fast-deep-equal')
const { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode, prettyPrintRoutesArray } = require('./lib/pretty-print')
Expand Down Expand Up @@ -73,6 +74,13 @@ function Router (opts) {
this.buildPrettyMeta = defaultBuildPrettyMeta
}

if (opts.querystringParser) {
assert(typeof opts.querystringParser === 'function', 'querystringParser must be a function')
this.querystringParser = opts.querystringParser
} else {
this.querystringParser = (query) => query === '' ? {} : querystring.parse(query)
}

this.caseSensitive = opts.caseSensitive === undefined ? true : opts.caseSensitive
this.ignoreTrailingSlash = opts.ignoreTrailingSlash || false
this.maxParamLength = opts.maxParamLength || 100
Expand Down Expand Up @@ -318,8 +326,8 @@ Router.prototype.lookup = function lookup (req, res, ctx) {
var handle = this.find(req.method, req.url, this.constrainer.deriveConstraints(req, ctx))
if (handle === null) return this._defaultRoute(req, res, ctx)
return ctx === undefined
? handle.handler(req, res, handle.params, handle.store)
: handle.handler.call(ctx, req, res, handle.params, handle.store)
? handle.handler(req, res, handle.params, handle.store, handle.searchParams)
: handle.handler.call(ctx, req, res, handle.params, handle.store, handle.searchParams)
}

Router.prototype.find = function find (method, path, derivedConstraints) {
Expand All @@ -330,8 +338,13 @@ Router.prototype.find = function find (method, path, derivedConstraints) {
path = path.replace(FULL_PATH_REGEXP, '/')
}

let sanitizedUrl
let querystring

try {
path = safeDecodeURI(path)
sanitizedUrl = safeDecodeURI(path)
path = sanitizedUrl.path
querystring = sanitizedUrl.querystring
} catch (error) {
return this._onBadUrl(path)
}
Expand Down Expand Up @@ -361,8 +374,9 @@ Router.prototype.find = function find (method, path, derivedConstraints) {
if (handle !== null) {
return {
handler: handle.handler,
store: handle.store,
params: handle._createParamsObject(params),
store: handle.store
searchParams: this.querystringParser(querystring)
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions lib/url-sanitizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function decodeComponentChar (highCharCode, lowCharCode) {

function safeDecodeURI (path) {
let shouldDecode = false
let querystring = ''

for (let i = 1; i < path.length; i++) {
const charCode = path.charCodeAt(i)
Expand All @@ -59,14 +60,16 @@ function safeDecodeURI (path) {
// string with a `;` character (code 59), e.g. `/foo;jsessionid=123456`.
// Thus, we need to split on `;` as well as `?` and `#`.
} else if (charCode === 63 || charCode === 59 || charCode === 35) {
querystring = path.slice(i + 1)
path = path.slice(0, i)
break
}
}
return shouldDecode ? decodeURI(path) : path
const decodedPath = shouldDecode ? decodeURI(path) : path
return { path: decodedPath, querystring }
}

function safeDecodeURIComponent (uriComponent, startIndex = 0) {
function safeDecodeURIComponent (uriComponent, startIndex) {
let decoded = ''
let lastIndex = startIndex

Expand Down
47 changes: 47 additions & 0 deletions test/custom-querystring-parser.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict'

const t = require('tap')
const test = t.test
const querystring = require('querystring')
const FindMyWay = require('../')

test('Custom querystring parser', t => {
t.plan(2)

const findMyWay = FindMyWay({
querystringParser: function (str) {
t.equal(str, 'foo=bar&baz=faz')
return querystring.parse(str)
}
})
findMyWay.on('GET', '/', () => {})

t.same(findMyWay.find('GET', '/?foo=bar&baz=faz').searchParams, { foo: 'bar', baz: 'faz' })
})

test('Custom querystring parser should be called also if there is nothing to parse', t => {
t.plan(2)

const findMyWay = FindMyWay({
querystringParser: function (str) {
t.equal(str, '')
return querystring.parse(str)
}
})
findMyWay.on('GET', '/', () => {})

t.same(findMyWay.find('GET', '/').searchParams, {})
})

test('Querystring without value', t => {
t.plan(2)

const findMyWay = FindMyWay({
querystringParser: function (str) {
t.equal(str, 'foo')
return querystring.parse(str)
}
})
findMyWay.on('GET', '/', () => {})
t.same(findMyWay.find('GET', '/?foo').searchParams, { foo: '' })
})
8 changes: 4 additions & 4 deletions test/methods.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ test('find should return the route', t => {

t.same(
findMyWay.find('GET', '/test'),
{ handler: fn, params: {}, store: null }
{ handler: fn, params: {}, store: null, searchParams: {} }
)
})

Expand All @@ -448,7 +448,7 @@ test('find should return the route with params', t => {

t.same(
findMyWay.find('GET', '/test/hello'),
{ handler: fn, params: { id: 'hello' }, store: null }
{ handler: fn, params: { id: 'hello' }, store: null, searchParams: {} }
)
})

Expand All @@ -471,7 +471,7 @@ test('should decode the uri - parametric', t => {

t.same(
findMyWay.find('GET', '/test/he%2Fllo'),
{ handler: fn, params: { id: 'he/llo' }, store: null }
{ handler: fn, params: { id: 'he/llo' }, store: null, searchParams: {} }
)
})

Expand All @@ -484,7 +484,7 @@ test('should decode the uri - wildcard', t => {

t.same(
findMyWay.find('GET', '/test/he%2Fllo'),
{ handler: fn, params: { '*': 'he/llo' }, store: null }
{ handler: fn, params: { '*': 'he/llo' }, store: null, searchParams: {} }
)
})

Expand Down
15 changes: 9 additions & 6 deletions test/querystring.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,35 @@ const test = t.test
const FindMyWay = require('../')

test('should sanitize the url - query', t => {
t.plan(1)
t.plan(2)
const findMyWay = FindMyWay()

findMyWay.on('GET', '/test', (req, res, params) => {
findMyWay.on('GET', '/test', (req, res, params, store, query) => {
t.same(query, { hello: 'world' })
t.ok('inside the handler')
})

findMyWay.lookup({ method: 'GET', url: '/test?hello=world', headers: {} }, null)
})

test('should sanitize the url - hash', t => {
t.plan(1)
t.plan(2)
const findMyWay = FindMyWay()

findMyWay.on('GET', '/test', (req, res, params) => {
findMyWay.on('GET', '/test', (req, res, params, store, query) => {
t.same(query, { hello: '' })
t.ok('inside the handler')
})

findMyWay.lookup({ method: 'GET', url: '/test#hello', headers: {} }, null)
})

test('handles path and query separated by ;', t => {
t.plan(1)
t.plan(2)
const findMyWay = FindMyWay()

findMyWay.on('GET', '/test', (req, res, params) => {
findMyWay.on('GET', '/test', (req, res, params, store, query) => {
t.same(query, { jsessionid: '123456' })
t.ok('inside the handler')
})

Expand Down
3 changes: 2 additions & 1 deletion test/store.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ test('find a store object', t => {
t.same(findMyWay.find('GET', '/test'), {
handler: fn,
params: {},
store: { hello: 'world' }
store: { hello: 'world' },
searchParams: {}
})
})

Expand Down