Skip to content

Commit

Permalink
Cleanup for hapijs#3822
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Nov 7, 2018
1 parent fa8a55e commit 19acd71
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 149 deletions.
68 changes: 37 additions & 31 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [`server.options.mime`](#server.options.mime)
- [`server.options.plugins`](#server.options.plugins)
- [`server.options.port`](#server.options.port)
- [`server.options.query`](#server.options.query)
- [`server.options.router`](#server.options.router)
- [`server.options.routes`](#server.options.routes)
- [`server.options.state`](#server.options.state)
Expand Down Expand Up @@ -498,6 +499,33 @@ The TCP port the server will listen to. Defaults the next available port when th
If `port` is a string containing a '/' character, it is used as a UNIX domain socket path.
If it starts with '\\.\pipe', it is used as a Windows named pipe.

#### <a name="server.options.query" /> `server.options.query`

Default value: `{}`.

Defines server handling of the request path query component.

##### <a name="server.options.query.parser" /> `server.options.query.parser`

Default value: none.

Sets a query parameters parser method using the signature `function(searchParams)` where:

- `query` - an object containing the incoming [`request.query`](#request.query) parameters.
- the method must return an object where each key is a parameter and matching value is the
parameter value. If the method throws, the error is used as the response or returned when
[`request.setUrl()`](#request.setUrl()) is called.

```js
const Qs = require('qs');

const options = {
query: {
parser: (query) => Qs.parse(query)
}
};
```

#### <a name="server.options.router" /> `server.options.router`

Default value: `{ isCaseSensitive: true, stripTrailingSlash: false }`.
Expand Down Expand Up @@ -3614,6 +3642,8 @@ the same. The following is the complete list of steps a request can go through:
and [`request.setMethod()`](#request.setMethod()) methods. Changes to the request path or
method will impact how the request is routed and can be used for rewrite rules.
- [`request.route`](#request.route) is unassigned.
- [`request.url`](#request.url) can be `null` if the incoming request path is invalid.
- [`request.path`](#request.path) can be an invalid path.
- JSONP configuration is ignored for any response returned from the extension point since no
route is matched yet and the JSONP configuration is unavailable.

Expand Down Expand Up @@ -4739,10 +4769,9 @@ Same as `pre` but represented as the response object created by the pre method.

Access: read only.

By default the object outputted from [node's URL parse()](https://nodejs.org/docs/latest/api/url.html#url_urlobject_query)
method. Might also be set indirectly via [request.setUrl](#request.setUrl())
in which case it may be a `string` (if `url` is set to an object with the `query` attribute as an
unparsed string).
An object where each key is a query parameter name and each matching value is the parameter value
or an array of values if a parameter repeats. Can be modified indirectly via
[request.setUrl](#request.setUrl()).

#### <a name="request.raw" /> `request.raw`

Expand Down Expand Up @@ -4919,11 +4948,10 @@ Can only be called from an `'onRequest'` extension method.
### <a name="request.setUrl()" /> `request.setUrl(url, [stripTrailingSlash]`

Changes the request URI before the router begins processing the request where:
- `url` - the new request URI. If `url` is a string, it is parsed with [node's **URL**
`parse()`](https://nodejs.org/docs/latest/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost)
method with `parseQueryString` set to `true`. `url` can also be set to an object
compatible with node's **URL** `parse()` method output.
- `stripTrailingSlash` - if `true`, strip the trailing slash from the path. Defaults to `false`.
- `url` - the new request URI. `url` can be a string or an instance of
[`Url.URL`](https://nodejs.org/dist/latest-v10.x/docs/api/url.html#url_class_url) in which case
`url.href` is used.
- `stripTrailingSlash` - if `true`, strip the trailing slash from the path. Defaults to `false`.

```js
const Hapi = require('hapi');
Expand All @@ -4939,28 +4967,6 @@ const onRequest = function (request, h) {
server.ext('onRequest', onRequest);
```

To use another query string parser:

```js
const Url = require('url');
const Hapi = require('hapi');
const Qs = require('qs');
const server = Hapi.server({ port: 80 });
const onRequest = function (request, h) {
const uri = request.url.href;
const parsed = Url.parse(uri, false);
parsed.query = Qs.parse(parsed.query);
request.setUrl(parsed);
return h.continue;
};
server.ext('onRequest', onRequest);
```

Can only be called from an `'onRequest'` extension method.

## Plugins
Expand Down
5 changes: 4 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ internals.routeBase = Joi.object({
})
.default(),
plugins: Joi.object(),
queryParser: Joi.func().allow(null).default(null),
response: Joi.object({
emptyStatusCode: Joi.valid(200, 204).default(200),
failAction: internals.failAction,
Expand Down Expand Up @@ -259,6 +258,10 @@ internals.server = Joi.object({
Joi.string().regex(/^\\\\\.\\pipe\\/) // Windows named pipe
])
.allow(null),
query: Joi.object({
parser: Joi.func()
})
.default(),
router: Joi.object({
isCaseSensitive: Joi.boolean().default(true),
stripTrailingSlash: Joi.boolean().default(false)
Expand Down
130 changes: 61 additions & 69 deletions lib/request.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const { URL, URLSearchParams } = require('url');
const Url = require('url');

const Boom = require('boom');
const Bounce = require('bounce');
Expand Down Expand Up @@ -34,6 +34,7 @@ exports = module.exports = internals.Request = class {
this._serverTimeoutId = null;
this._states = {};
this._transmitted = false; // Indicates whether a response has been successful sent
this._urlError = null;

this.app = (options.app ? Object.assign({}, options.app) : {}); // Place for application-specific state without conflicts with hapi, should not be used by plugins (shallow cloned)
this.headers = req.headers;
Expand Down Expand Up @@ -70,13 +71,7 @@ exports = module.exports = internals.Request = class {

// Parse request url

try {
this.setUrl(req.url, this._core.settings.router.stripTrailingSlash);
}
catch (err) {
Bounce.ignore(err, 'boom');
this.url = err;
}
this._initializeUrl();
}

static generate(server, req, res, options) {
Expand Down Expand Up @@ -105,31 +100,38 @@ exports = module.exports = internals.Request = class {
return this._events;
}

_initializeUrl() {

try {
this._setUrl(this.raw.req.url, this._core.settings.router.stripTrailingSlash);
}
catch (err) {
this.path = this.raw.req.url;
this.query = {};

this._urlError = Boom.boomify(err, { statusCode: 400, override: false });
}
}

setUrl(url, stripTrailingSlash) {

Hoek.assert(this.params === null, 'Cannot change request URL after routing');

if (url instanceof URL) {
if (url instanceof Url.URL) {
url = url.href;
}

Hoek.assert(typeof url === 'string', 'Url must be a string or URL object');

const parseFull = url.length === 0 || url[0] !== '/';
try {
if (parseFull) {
url = new URL(url);
}
else {
const hostname = this.info.host || `${this._core.info.host}:${this._core.info.port}`;
url = new URL(url, `${this._core.info.protocol}://${hostname}`);
}
}
catch (err) {
Bounce.ignore(err, TypeError);
this._setUrl(url, stripTrailingSlash);
this._urlError = null;
}

throw Boom.boomify(err, { statusCode: 400 });
}
_setUrl(url, stripTrailingSlash) {

const base = (url[0] === '/' ? `${this._core.info.protocol}://${this.info.host || `${this._core.info.host}:${this._core.info.port}`}` : undefined);

url = new Url.URL(url, base);

// Apply path modifications

Expand All @@ -144,15 +146,46 @@ exports = module.exports = internals.Request = class {

url.pathname = path;

// Parse query (must be done before this.url is set in case query parsing throws)

this.query = this._parseQuery(url.searchParams);

// Store request properties

this.url = url;
this.path = path;

if (parseFull) {
this.info.hostname = url.hostname;
this.info.host = url.host;
this.info.hostname = url.hostname;
this.info.host = url.host;
}

_parseQuery(searchParams) {

// Flatten map

let query = Object.create(null);
for (let [key, value] of searchParams) {
const entry = query[key];
if (entry !== undefined) {
value = [].concat(entry, value);
}

query[key] = value;
}

// Custom parser

const parser = this._core.settings.query.parser;
if (parser) {
query = parser(query);
if (!query ||
typeof query !== 'object') {

throw Boom.badImplementation('Parsed query must be an object');
}
}

return query;
}

setMethod(method) {
Expand Down Expand Up @@ -181,7 +214,6 @@ exports = module.exports = internals.Request = class {
}

this._lookup();
this._queryParse();
this._setTimeouts();
await this._lifecycle();
this._reply();
Expand All @@ -204,8 +236,8 @@ exports = module.exports = internals.Request = class {

// Validate path

if (this.url instanceof Error) {
throw this.url;
if (this._urlError) {
throw this._urlError;
}
}

Expand Down Expand Up @@ -240,46 +272,6 @@ exports = module.exports = internals.Request = class {
}
}

_queryParse() {

const { queryParser } = this._route.settings;

const baseParser = (iterator) => {

const query = Object.create(null);
for (let [key, value] of iterator) {
const entry = query[key];
if (entry !== undefined) {
value = [].concat(entry, value);
}

query[key] = value;
}

return query;
};

if (queryParser) {
try {
let result = queryParser(this);

Hoek.assert(typeof result === 'object' && result !== null, 'Parsed query must be an object');

if (result instanceof URLSearchParams || result instanceof Map) {
result = baseParser(result);
}

this.query = result;
}
catch (err) {
return this._reply(err);
}
}
else {
this.query = baseParser(this.url.searchParams);
}
}

_setTimeouts() {

if (this.raw.req.socket &&
Expand Down
Loading

0 comments on commit 19acd71

Please sign in to comment.