Skip to content
Open
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
6 changes: 3 additions & 3 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

'use strict';

var url = require('url');
var sprintf = require('util').format;

var assert = require('assert-plus');
Expand All @@ -11,6 +10,7 @@ var Negotiator = require('negotiator');
var uuid = require('uuid');

var dtrace = require('./dtrace');
var utils = require('./utils');

///-- Helpers
/**
Expand Down Expand Up @@ -74,7 +74,7 @@ function patch(Request) {

var protocol = this.isSecure() ? 'https://' : 'http://';
var hostname = this.headers.host;
return url.resolve(protocol + hostname + this.path() + '/', path);
return new URL(path, protocol + hostname + this.path() + '/').href;
};

/**
Expand Down Expand Up @@ -424,7 +424,7 @@ function patch(Request) {
*/
Request.prototype.getUrl = function getUrl() {
if (this._cacheURL !== this.url) {
this._url = url.parse(this.url);
this._url = utils.parseRequestUrl(this.url);
this._cacheURL = this.url;
}
return this._url;
Expand Down
53 changes: 31 additions & 22 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

var http = require('http');
var sprintf = require('util').format;
var url = require('url');

var assert = require('assert-plus');
var mime = require('mime');
Expand Down Expand Up @@ -740,6 +739,7 @@ function patch(Response) {
var self = this;
var statusCode = 302;
var finalUri;
var finalHostname;
var redirectLocation;
var next;

Expand Down Expand Up @@ -768,32 +768,32 @@ function patch(Response) {
? opt.secure
: req.isSecure();

// if hostname is passed in, use that as the base,
// otherwise fall back on current url.
var parsedUri = url.parse(opt.hostname || currentFullPath, true);

// create the object we'll use to format for the final uri.
// this object will eventually get passed to url.format().
// can't use parsedUri to seed it, as it confuses the url module
// with some existing parsed state. instead, we'll pick the things
// we want and use that as a starting point.
finalUri = {
port: parsedUri.port,
hostname: parsedUri.hostname,
query: parsedUri.query,
pathname: parsedUri.pathname
};
// if hostname is passed in, use that as the base, otherwise
// fall back on current url. mutate a URL instance directly
// instead of bouncing through a plain-object descriptor.
var raw = opt.hostname || currentFullPath;
var isAbsolute = raw.indexOf('://') !== -1;
finalUri = isAbsolute
? new URL(raw)
: new URL(raw, 'http://localhost');

// start building url based on options.
// start with the host
if (opt.hostname) {
finalUri.hostname = opt.hostname;
// an explicit hostname replaces the current path entirely,
// so start from a clean slate.
finalUri.pathname = '/';
finalUri.search = '';
}

// hostname is only known if it was passed in explicitly, or if
// the current url was itself absolute (e.g. proxied requests).
finalHostname =
opt.hostname || (isAbsolute ? finalUri.hostname : null);

// then set protocol IFF hostname is set - otherwise we end up with
// malformed URL.
if (finalUri.hostname) {
finalUri.protocol = secure === true ? 'https' : 'http';
if (finalHostname) {
finalUri.protocol = secure === true ? 'https:' : 'http:';
finalUri.hostname = finalHostname;
}

// then set current path after the host
Expand Down Expand Up @@ -832,7 +832,16 @@ function patch(Response) {
return next(new InternalServerError('could not construct url'));
}

redirectLocation = url.format(finalUri);
if (typeof finalUri === 'string') {
redirectLocation = finalUri;
} else {
// a URL instance can't represent a host-less location, so build
// that case by hand; otherwise the instance already has
// everything it needs to serialize itself.
redirectLocation = finalHostname
? finalUri.toString()
: finalUri.pathname + (finalUri.search || '');
}

self.emit('redirect', redirectLocation);

Expand Down
53 changes: 52 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,60 @@ function mergeQs(obj1, obj2) {
return merged;
}

/**
* Parses a raw HTTP request URL string into a legacy url.parse-shaped object.
* Handles the special OPTIONS '*' request-target.
*
* @public
* @function parseRequestUrl
* @param {String} rawUrl - the raw request URL string
* @returns {Object} url descriptor with pathname, search, query, href etc.
*/
function parseRequestUrl(rawUrl) {
var pathname, search, hash, port, hostname;

if (rawUrl === '*') {
pathname = '*';
search = null;
hash = null;
port = null;
hostname = null;
} else if (rawUrl.indexOf('://') !== -1) {
var absParsed = new URL(rawUrl);
pathname = absParsed.pathname;
search = absParsed.search || null;
hash = absParsed.hash || null;
port = absParsed.port || null;
hostname = absParsed.hostname || null;
} else {
var relParsed = new URL(rawUrl, 'http://localhost');
pathname = relParsed.pathname;
search = relParsed.search || null;
hash = relParsed.hash || null;
port = null;
hostname = null;
}

return {
protocol: null,
slashes: null,
auth: null,
host: null,
port: port,
hostname: hostname,
hash: hash,
search: search,
query: search ? search.slice(1) : null,
pathname: pathname,
path: pathname + (search || ''),
href: rawUrl
};
}

///--- Exports

module.exports = {
shallowCopy: shallowCopy,
mergeQs: mergeQs
mergeQs: mergeQs,
parseRequestUrl: parseRequestUrl
};
83 changes: 83 additions & 0 deletions test/request.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,86 @@ test(
});
}
);

test(
module,
'getUrl should return correct shape for path with no query',
function(t) {
SERVER.get('/geturl-plain', function(req, res, next) {
var u = req.getUrl();
t.equal(u.href, '/geturl-plain');
t.equal(u.pathname, '/geturl-plain');
t.equal(u.search, null);
t.equal(u.query, null);
t.equal(u.hash, null);
t.equal(u.host, null);
t.equal(u.hostname, null);
t.equal(u.port, null);
t.equal(u.protocol, null);
res.send();
return next();
});

CLIENT.get('/geturl-plain', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
}
);

test(module, 'getUrl should return correct query string', function(t) {
SERVER.get('/geturl-qs', function(req, res, next) {
var u = req.getUrl();
t.equal(u.href, '/geturl-qs?a=1&b=2');
t.equal(u.pathname, '/geturl-qs');
t.equal(u.search, '?a=1&b=2');
t.equal(u.query, 'a=1&b=2');
t.equal(u.hash, null);
t.equal(u.host, null);
t.equal(u.hostname, null);
t.equal(u.port, null);
t.equal(u.protocol, null);
res.send();
return next();
});

CLIENT.get('/geturl-qs?a=1&b=2', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});

test(module, 'getUrl should handle OPTIONS * request-target', function(t) {
SERVER.opts('*', function(req, res, next) {
var u = req.getUrl();
t.equal(u.pathname, '*');
t.equal(u.search, null);
t.equal(u.query, null);
res.send(200);
return next();
});

CLIENT.opts('*', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});

test(module, 'getUrl result is cached across calls', function(t) {
SERVER.get('/geturl-cache', function(req, res, next) {
var u1 = req.getUrl();
var u2 = req.getUrl();
t.strictEqual(u1, u2);
res.send();
return next();
});

CLIENT.get('/geturl-cache', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
Loading