diff --git a/benchmark/url/legacy-vs-whatwg-url-get-prop.js b/benchmark/url/legacy-vs-whatwg-url-get-prop.js
new file mode 100644
index 00000000000000..229a4e60652b64
--- /dev/null
+++ b/benchmark/url/legacy-vs-whatwg-url-get-prop.js
@@ -0,0 +1,97 @@
+'use strict';
+const common = require('../common.js');
+const url = require('url');
+const URL = url.URL;
+const assert = require('assert');
+const inputs = require('../fixtures/url-inputs.js').urls;
+
+const bench = common.createBenchmark(main, {
+  type: Object.keys(inputs),
+  method: ['legacy', 'whatwg'],
+  n: [1e5]
+});
+
+// At the time of writing, when using a passed property name to index
+// the object, Crankshaft would generate a LoadKeyedGeneric even when it
+// remains a constant in the function, so here we must use the literal
+// instead to get a LoadNamedField.
+function useLegacy(n, input) {
+  const obj = url.parse(input);
+  const noDead = {
+    protocol: obj.protocol,
+    auth: obj.auth,
+    host: obj.host,
+    hostname: obj.hostname,
+    port: obj.port,
+    pathname: obj.pathname,
+    search: obj.search,
+    hash: obj.hash
+  };
+  // It's necessary to assign the values to an object
+  // to avoid loop invariant code motion.
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    noDead.protocol = obj.protocol;
+    noDead.auth = obj.auth;
+    noDead.host = obj.host;
+    noDead.hostname = obj.hostname;
+    noDead.port = obj.port;
+    noDead.pathname = obj.pathname;
+    noDead.search = obj.search;
+    noDead.hash = obj.hash;
+  }
+  bench.end(n);
+  return noDead;
+}
+
+function useWHATWG(n, input) {
+  const obj = new URL(input);
+  const noDead = {
+    protocol: obj.protocol,
+    auth: `${obj.username}:${obj.password}`,
+    host: obj.host,
+    hostname: obj.hostname,
+    port: obj.port,
+    pathname: obj.pathname,
+    search: obj.search,
+    hash: obj.hash
+  };
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    noDead.protocol = obj.protocol;
+    noDead.auth = `${obj.username}:${obj.password}`;
+    noDead.host = obj.host;
+    noDead.hostname = obj.hostname;
+    noDead.port = obj.port;
+    noDead.pathname = obj.pathname;
+    noDead.search = obj.search;
+    noDead.hash = obj.hash;
+  }
+  bench.end(n);
+  return noDead;
+}
+
+function main(conf) {
+  const type = conf.type;
+  const n = conf.n | 0;
+  const method = conf.method;
+
+  const input = inputs[type];
+  if (!input) {
+    throw new Error('Unknown input type');
+  }
+
+  var noDead;  // Avoid dead code elimination.
+  switch (method) {
+    case 'legacy':
+      noDead = useLegacy(n, input);
+      break;
+    case 'whatwg':
+      noDead = useWHATWG(n, input);
+      break;
+    default:
+      throw new Error('Unknown method');
+  }
+
+  assert.ok(noDead);
+}
diff --git a/benchmark/url/legacy-vs-whatwg-url-parse.js b/benchmark/url/legacy-vs-whatwg-url-parse.js
new file mode 100644
index 00000000000000..ec386b7b85597d
--- /dev/null
+++ b/benchmark/url/legacy-vs-whatwg-url-parse.js
@@ -0,0 +1,57 @@
+'use strict';
+const common = require('../common.js');
+const url = require('url');
+const URL = url.URL;
+const assert = require('assert');
+const inputs = require('../fixtures/url-inputs.js').urls;
+
+const bench = common.createBenchmark(main, {
+  type: Object.keys(inputs),
+  method: ['legacy', 'whatwg'],
+  n: [1e5]
+});
+
+function useLegacy(n, input) {
+  var noDead = url.parse(input);
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    noDead = url.parse(input);
+  }
+  bench.end(n);
+  return noDead;
+}
+
+function useWHATWG(n, input) {
+  var noDead = new URL(input);
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    noDead = new URL(input);
+  }
+  bench.end(n);
+  return noDead;
+}
+
+function main(conf) {
+  const type = conf.type;
+  const n = conf.n | 0;
+  const method = conf.method;
+
+  const input = inputs[type];
+  if (!input) {
+    throw new Error('Unknown input type');
+  }
+
+  var noDead;  // Avoid dead code elimination.
+  switch (method) {
+    case 'legacy':
+      noDead = useLegacy(n, input);
+      break;
+    case 'whatwg':
+      noDead = useWHATWG(n, input);
+      break;
+    default:
+      throw new Error('Unknown method');
+  }
+
+  assert.ok(noDead);
+}
diff --git a/benchmark/url/legacy-vs-whatwg-url-searchparams-parse.js b/benchmark/url/legacy-vs-whatwg-url-searchparams-parse.js
new file mode 100644
index 00000000000000..b4a80af4e5eabd
--- /dev/null
+++ b/benchmark/url/legacy-vs-whatwg-url-searchparams-parse.js
@@ -0,0 +1,51 @@
+'use strict';
+const common = require('../common.js');
+const { URLSearchParams } = require('url');
+const querystring = require('querystring');
+const inputs = require('../fixtures/url-inputs.js').searchParams;
+
+const bench = common.createBenchmark(main, {
+  type: Object.keys(inputs),
+  method: ['legacy', 'whatwg'],
+  n: [1e6]
+});
+
+function useLegacy(n, input) {
+  querystring.parse(input);
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    querystring.parse(input);
+  }
+  bench.end(n);
+}
+
+function useWHATWG(n, input) {
+  new URLSearchParams(input);
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    new URLSearchParams(input);
+  }
+  bench.end(n);
+}
+
+function main(conf) {
+  const type = conf.type;
+  const n = conf.n | 0;
+  const method = conf.method;
+
+  const input = inputs[type];
+  if (!input) {
+    throw new Error('Unknown input type');
+  }
+
+  switch (method) {
+    case 'legacy':
+      useLegacy(n, input);
+      break;
+    case 'whatwg':
+      useWHATWG(n, input);
+      break;
+    default:
+      throw new Error('Unknown method');
+  }
+}
diff --git a/benchmark/url/legacy-vs-whatwg-url-searchparams-serialize.js b/benchmark/url/legacy-vs-whatwg-url-searchparams-serialize.js
new file mode 100644
index 00000000000000..2b8d2c36a810b3
--- /dev/null
+++ b/benchmark/url/legacy-vs-whatwg-url-searchparams-serialize.js
@@ -0,0 +1,53 @@
+'use strict';
+const common = require('../common.js');
+const { URLSearchParams } = require('url');
+const querystring = require('querystring');
+const inputs = require('../fixtures/url-inputs.js').searchParams;
+
+const bench = common.createBenchmark(main, {
+  type: Object.keys(inputs),
+  method: ['legacy', 'whatwg'],
+  n: [1e6]
+});
+
+function useLegacy(n, input, prop) {
+  const obj = querystring.parse(input);
+  querystring.stringify(obj);
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    querystring.stringify(obj);
+  }
+  bench.end(n);
+}
+
+function useWHATWG(n, input, prop) {
+  const obj = new URLSearchParams(input);
+  obj.toString();
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    obj.toString();
+  }
+  bench.end(n);
+}
+
+function main(conf) {
+  const type = conf.type;
+  const n = conf.n | 0;
+  const method = conf.method;
+
+  const input = inputs[type];
+  if (!input) {
+    throw new Error('Unknown input type');
+  }
+
+  switch (method) {
+    case 'legacy':
+      useLegacy(n, input);
+      break;
+    case 'whatwg':
+      useWHATWG(n, input);
+      break;
+    default:
+      throw new Error('Unknown method');
+  }
+}
diff --git a/benchmark/url/legacy-vs-whatwg-url-serialize.js b/benchmark/url/legacy-vs-whatwg-url-serialize.js
new file mode 100644
index 00000000000000..35b459a10c0e0b
--- /dev/null
+++ b/benchmark/url/legacy-vs-whatwg-url-serialize.js
@@ -0,0 +1,59 @@
+'use strict';
+const common = require('../common.js');
+const url = require('url');
+const URL = url.URL;
+const assert = require('assert');
+const inputs = require('../fixtures/url-inputs.js').urls;
+
+const bench = common.createBenchmark(main, {
+  type: Object.keys(inputs),
+  method: ['legacy', 'whatwg'],
+  n: [1e5]
+});
+
+function useLegacy(n, input, prop) {
+  const obj = url.parse(input);
+  var noDead = url.format(obj);
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    noDead = url.format(obj);
+  }
+  bench.end(n);
+  return noDead;
+}
+
+function useWHATWG(n, input, prop) {
+  const obj = new URL(input);
+  var noDead = obj.toString();
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    noDead = obj.toString();
+  }
+  bench.end(n);
+  return noDead;
+}
+
+function main(conf) {
+  const type = conf.type;
+  const n = conf.n | 0;
+  const method = conf.method;
+
+  const input = inputs[type];
+  if (!input) {
+    throw new Error('Unknown input type');
+  }
+
+  var noDead;  // Avoid dead code elimination.
+  switch (method) {
+    case 'legacy':
+      noDead = useLegacy(n, input);
+      break;
+    case 'whatwg':
+      noDead = useWHATWG(n, input);
+      break;
+    default:
+      throw new Error('Unknown method');
+  }
+
+  assert.ok(noDead);
+}
diff --git a/benchmark/url/url-searchparams-iteration.js b/benchmark/url/url-searchparams-iteration.js
new file mode 100644
index 00000000000000..0f4b71a0a183dd
--- /dev/null
+++ b/benchmark/url/url-searchparams-iteration.js
@@ -0,0 +1,61 @@
+'use strict';
+const common = require('../common.js');
+const assert = require('assert');
+const { URLSearchParams } = require('url');
+
+const bench = common.createBenchmark(main, {
+  method: ['forEach', 'iterator'],
+  n: [1e6]
+});
+
+const str = 'one=single&two=first&three=first&two=2nd&three=2nd&three=3rd';
+
+function forEach(n) {
+  const params = new URLSearchParams(str);
+  const noDead = [];
+  const cb = (val, key) => {
+    noDead[0] = key;
+    noDead[1] = val;
+  };
+
+  bench.start();
+  for (var i = 0; i < n; i += 1)
+    params.forEach(cb);
+  bench.end(n);
+
+  assert.strictEqual(noDead[0], 'three');
+  assert.strictEqual(noDead[1], '3rd');
+}
+
+function iterator(n) {
+  const params = new URLSearchParams(str);
+  const noDead = [];
+
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    for (const pair of params) {
+      noDead[0] = pair[0];
+      noDead[1] = pair[1];
+    }
+  }
+  bench.end(n);
+
+  assert.strictEqual(noDead[0], 'three');
+  assert.strictEqual(noDead[1], '3rd');
+}
+
+function main(conf) {
+  const method = conf.method;
+  const n = conf.n | 0;
+
+  switch (method) {
+    case 'forEach':
+      forEach(n);
+      break;
+    case 'iterator':
+      iterator(n);
+      break;
+    default:
+      throw new Error('Unknown method');
+  }
+}
diff --git a/benchmark/url/url-searchparams-read.js b/benchmark/url/url-searchparams-read.js
new file mode 100644
index 00000000000000..762ffcca03d69d
--- /dev/null
+++ b/benchmark/url/url-searchparams-read.js
@@ -0,0 +1,58 @@
+'use strict';
+const common = require('../common.js');
+const { URLSearchParams } = require('url');
+
+const bench = common.createBenchmark(main, {
+  method: ['get', 'getAll', 'has'],
+  param: ['one', 'two', 'three', 'nonexistent'],
+  n: [2e7]
+});
+
+const str = 'one=single&two=first&three=first&two=2nd&three=2nd&three=3rd';
+
+function get(n, param) {
+  const params = new URLSearchParams(str);
+
+  bench.start();
+  for (var i = 0; i < n; i += 1)
+    params.get(param);
+  bench.end(n);
+}
+
+function getAll(n, param) {
+  const params = new URLSearchParams(str);
+
+  bench.start();
+  for (var i = 0; i < n; i += 1)
+    params.getAll(param);
+  bench.end(n);
+}
+
+function has(n, param) {
+  const params = new URLSearchParams(str);
+
+  bench.start();
+  for (var i = 0; i < n; i += 1)
+    params.has(param);
+  bench.end(n);
+}
+
+function main(conf) {
+  const method = conf.method;
+  const param = conf.param;
+  const n = conf.n | 0;
+
+  switch (method) {
+    case 'get':
+      get(n, param);
+      break;
+    case 'getAll':
+      getAll(n, param);
+      break;
+    case 'has':
+      has(n, param);
+      break;
+    default:
+      throw new Error('Unknown method');
+  }
+}
diff --git a/benchmark/url/url-searchparams-sort.js b/benchmark/url/url-searchparams-sort.js
new file mode 100644
index 00000000000000..677ce511cf3ea2
--- /dev/null
+++ b/benchmark/url/url-searchparams-sort.js
@@ -0,0 +1,48 @@
+'use strict';
+const common = require('../common.js');
+const URLSearchParams = require('url').URLSearchParams;
+
+const inputs = {
+  empty: '',
+  sorted: 'a&b&c&d&e&f&g&h&i&j&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z',
+  almostsorted: 'a&b&c&d&e&f&g&i&h&j&k&l&m&n&o&p&q&r&s&t&u&w&v&x&y&z',
+  reversed: 'z&y&x&w&v&u&t&s&r&q&p&o&n&m&l&k&j&i&h&g&f&e&d&c&b&a',
+  random: 'm&t&d&c&z&v&a&n&p&y&u&o&h&l&f&j&e&q&b&i&s&x&k&w&r&g',
+  // 8 parameters
+  short: 'm&t&d&c&z&v&a&n',
+  // 88 parameters
+  long: 'g&r&t&h&s&r&d&w&b&n&h&k&x&m&k&h&o&e&x&c&c&g&e&b&p&p&s&n&j&b&y&z&' +
+        'u&l&o&r&w&a&u&l&m&f&j&q&p&f&e&y&e&n&e&l&m&w&u&w&t&n&t&q&v&y&c&o&' +
+        'k&f&j&i&l&m&g&j&d&i&z&q&p&x&q&q&d&n&y&w&g&i&v&r'
+};
+
+function getParams(str) {
+  const out = [];
+  for (const key of str.split('&')) {
+    out.push(key, '');
+  }
+  return out;
+}
+
+const bench = common.createBenchmark(main, {
+  type: Object.keys(inputs),
+  n: [1e6]
+}, {
+  flags: ['--expose-internals']
+});
+
+function main(conf) {
+  const searchParams = require('internal/url').searchParamsSymbol;
+  const input = inputs[conf.type];
+  const n = conf.n | 0;
+  const params = new URLSearchParams();
+  const array = getParams(input);
+
+  var i;
+  bench.start();
+  for (i = 0; i < n; i++) {
+    params[searchParams] = array.slice();
+    params.sort();
+  }
+  bench.end(n);
+}
diff --git a/benchmark/url/usvstring.js b/benchmark/url/usvstring.js
new file mode 100644
index 00000000000000..40a945037385cf
--- /dev/null
+++ b/benchmark/url/usvstring.js
@@ -0,0 +1,28 @@
+'use strict';
+const common = require('../common.js');
+
+const inputs = {
+  valid: 'adsfadsfadsf',
+  validsurr: '\uda23\ude23\uda1f\udfaa\ud800\udfff\uda23\ude23\uda1f\udfaa' +
+             '\ud800\udfff',
+  someinvalid: 'asasfdfasd\uda23',
+  allinvalid: '\udc45\uda23 \udf00\udc00 \udfaa\uda12 \udc00\udfaa',
+  nonstring: { toString() { return 'asdf'; } }
+};
+const bench = common.createBenchmark(main, {
+  input: Object.keys(inputs),
+  n: [5e7]
+}, {
+  flags: ['--expose-internals']
+});
+
+function main(conf) {
+  const { toUSVString } = require('internal/url');
+  const str = inputs[conf.input];
+  const n = conf.n | 0;
+
+  bench.start();
+  for (var i = 0; i < n; i++)
+    toUSVString(str);
+  bench.end(n);
+}
diff --git a/benchmark/url/whatwg-url-idna.js b/benchmark/url/whatwg-url-idna.js
new file mode 100644
index 00000000000000..3d0ea3dc8fe516
--- /dev/null
+++ b/benchmark/url/whatwg-url-idna.js
@@ -0,0 +1,45 @@
+'use strict';
+const common = require('../common.js');
+const { domainToASCII, domainToUnicode } = require('url');
+
+const inputs = {
+  empty: {
+    ascii: '',
+    unicode: ''
+  },
+  none: {
+    ascii: 'passports',
+    unicode: 'passports'
+  },
+  some: {
+    ascii: 'Paßstraße',
+    unicode: 'xn--Pastrae-1vae'
+  },
+  all: {
+    ascii: '他们不说中文',
+    unicode: 'xn--ihqwczyycu19kkg2c'
+  },
+  nonstring: {
+    ascii: { toString() { return ''; } },
+    unicode: { toString() { return ''; } }
+  }
+};
+
+const bench = common.createBenchmark(main, {
+  input: Object.keys(inputs),
+  to: ['ascii', 'unicode'],
+  n: [5e6]
+});
+
+function main(conf) {
+  const n = conf.n | 0;
+  const to = conf.to;
+  const input = inputs[conf.input][to];
+  const method = to === 'ascii' ? domainToASCII : domainToUnicode;
+
+  bench.start();
+  for (var i = 0; i < n; i++) {
+    method(input);
+  }
+  bench.end(n);
+}
diff --git a/benchmark/url/whatwg-url-properties.js b/benchmark/url/whatwg-url-properties.js
new file mode 100644
index 00000000000000..3a865d2335ab3c
--- /dev/null
+++ b/benchmark/url/whatwg-url-properties.js
@@ -0,0 +1,75 @@
+'use strict';
+const common = require('../common.js');
+const URL = require('url').URL;
+const inputs = require('../fixtures/url-inputs.js').urls;
+
+const bench = common.createBenchmark(main, {
+  input: Object.keys(inputs),
+  prop: ['href', 'origin', 'protocol',
+         'username', 'password', 'host', 'hostname', 'port',
+         'pathname', 'search', 'searchParams', 'hash'],
+  n: [3e5]
+});
+
+function setAndGet(n, url, prop, alternative) {
+  const old = url[prop];
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    url[prop] = n % 2 === 0 ? alternative : old;  // set
+    url[prop];  // get
+  }
+  bench.end(n);
+}
+
+function get(n, url, prop) {
+  bench.start();
+  for (var i = 0; i < n; i += 1) {
+    url[prop];  // get
+  }
+  bench.end(n);
+}
+
+const alternatives = {
+  href: 'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test',
+  protocol: 'https:',
+  username: 'user2',
+  password: 'pass2',
+  host: 'foo.bar.net:22',
+  hostname: 'foo.bar.org',
+  port: '23',
+  pathname: '/aaa/bbb',
+  search: '?k=99',
+  hash: '#abcd'
+};
+
+function getAlternative(prop) {
+  return alternatives[prop];
+}
+
+function main(conf) {
+  const n = conf.n | 0;
+  const input = inputs[conf.input];
+  const url = new URL(input);
+  const prop = conf.prop;
+
+  switch (prop) {
+    case 'protocol':
+    case 'username':
+    case 'password':
+    case 'host':
+    case 'hostname':
+    case 'port':
+    case 'pathname':
+    case 'search':
+    case 'hash':
+    case 'href':
+      setAndGet(n, url, prop, getAlternative(prop));
+      break;
+    case 'origin':
+    case 'searchParams':
+      get(n, url, prop);
+      break;
+    default:
+      throw new Error('Unknown prop');
+  }
+}
diff --git a/doc/api/url.md b/doc/api/url.md
index 6d0911da81d899..ee7f75c9032641 100644
--- a/doc/api/url.md
+++ b/doc/api/url.md
@@ -17,52 +17,803 @@ A URL string is a structured string containing multiple meaningful components.
 When parsed, a URL object is returned containing properties for each of these
 components.
 
-The following details each of the components of a parsed URL. The example
-`'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'` is used to
-illustrate each.
+The `url` module provides two APIs for working with URLs: a legacy API that is
+Node.js specific, and a newer API that implements the same
+[WHATWG URL Standard][] used by web browsers.
+
+*Note*: While the Legacy API has not been deprecated, it is maintained solely
+for backwards compatibility with existing applications. New application code
+should use the WHATWG API.
+
+A comparison between the WHATWG and Legacy APIs is provided below. Above the URL
+`'http://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash'`, properties of
+an object returned by the legacy `url.parse()` are shown. Below it are
+properties of a WHATWG `URL` object.
+
+*Note*: WHATWG URL's `origin` property includes `protocol` and `host`, but not
+`username` or `password`.
 
 ```txt
-┌─────────────────────────────────────────────────────────────────────────────┐
-│                                    href                                     │
-├──────────┬┬───────────┬─────────────────┬───────────────────────────┬───────┤
-│ protocol ││   auth    │      host       │           path            │ hash  │
-│          ││           ├──────────┬──────┼──────────┬────────────────┤       │
-│          ││           │ hostname │ port │ pathname │     search     │       │
-│          ││           │          │      │          ├─┬──────────────┤       │
-│          ││           │          │      │          │ │    query     │       │
-"  http:   // user:pass @ host.com : 8080   /p/a/t/h  ?  query=string   #hash "
-│          ││           │          │      │          │ │              │       │
-└──────────┴┴───────────┴──────────┴──────┴──────────┴─┴──────────────┴───────┘
+┌─────────────────────────────────────────────────────────────────────────────────────────────┐
+│                                            href                                             │
+├──────────┬──┬─────────────────────┬─────────────────────┬───────────────────────────┬───────┤
+│ protocol │  │        auth         │        host         │           path            │ hash  │
+│          │  │                     ├──────────────┬──────┼──────────┬────────────────┤       │
+│          │  │                     │   hostname   │ port │ pathname │     search     │       │
+│          │  │                     │              │      │          ├─┬──────────────┤       │
+│          │  │                     │              │      │          │ │    query     │       │
+"  https:   //    user   :   pass   @ sub.host.com : 8080   /p/a/t/h  ?  query=string   #hash "
+│          │  │          │          │   hostname   │ port │          │                │       │
+│          │  │          │          ├──────────────┴──────┤          │                │       │
+│ protocol │  │ username │ password │        host         │          │                │       │
+├──────────┴──┼──────────┴──────────┼─────────────────────┤          │                │       │
+│   origin    │                     │       origin        │ pathname │     search     │ hash  │
+├─────────────┴─────────────────────┴─────────────────────┴──────────┴────────────────┴───────┤
+│                                            href                                             │
+└─────────────────────────────────────────────────────────────────────────────────────────────┘
 (all spaces in the "" line should be ignored -- they are purely for formatting)
 ```
 
-### urlObject.href
+Parsing the URL string using the WHATWG API:
 
-The `href` property is the full URL string that was parsed with both the
-`protocol` and `host` components converted to lower-case.
+```js
+const { URL } = require('url');
+const myURL =
+  new URL('https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash');
+```
 
-For example: `'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'`
+*Note*: In Web Browsers, the WHATWG `URL` class is a global that is always
+available. In Node.js, however, the `URL` class must be accessed via
+`require('url').URL`.
 
-### urlObject.protocol
+Parsing the URL string using the Legacy API:
+
+```js
+const url = require('url');
+const myURL =
+  url.parse('https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash');
+```
 
-The `protocol` property identifies the URL's lower-cased protocol scheme.
+## The WHATWG URL API
+<!-- YAML
+added: v7.0.0, REPLACEME
+-->
 
-For example: `'http:'`
+### Class: URL
 
-### urlObject.slashes
+Browser-compatible `URL` class, implemented by following the WHATWG URL
+Standard. [Examples of parsed URLs][] may be found in the Standard itself.
 
-The `slashes` property is a `boolean` with a value of `true` if two ASCII
-forward-slash characters (`/`) are required following the colon in the
-`protocol`.
+*Note*: In accordance with browser conventions, all properties of `URL` objects
+are implemented as getters and setters on the class prototype, rather than as
+data properties on the object itself. Thus, unlike [legacy urlObject][]s, using
+the `delete` keyword on any properties of `URL` objects (e.g. `delete
+myURL.protocol`, `delete myURL.pathname`, etc) has no effect but will still
+return `true`.
 
-### urlObject.host
+#### Constructor: new URL(input[, base])
 
-The `host` property is the full lower-cased host portion of the URL, including
-the `port` if specified.
+* `input` {string} The input URL to parse
+* `base` {string|URL} The base URL to resolve against if the `input` is not
+  absolute.
+
+Creates a new `URL` object by parsing the `input` relative to the `base`. If
+`base` is passed as a string, it will be parsed equivalent to `new URL(base)`.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('/foo', 'https://example.org/');
+// https://example.org/foo
+```
+
+A `TypeError` will be thrown if the `input` or `base` are not valid URLs. Note
+that an effort will be made to coerce the given values into strings. For
+instance:
+
+```js
+const { URL } = require('url');
+const myURL = new URL({ toString: () => 'https://example.org/' });
+// https://example.org/
+```
+
+Unicode characters appearing within the hostname of `input` will be
+automatically converted to ASCII using the [Punycode][] algorithm.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://你好你好');
+// https://xn--6qqa088eba/
+```
+
+*Note*: This feature is only available if the `node` executable was compiled
+with [ICU][] enabled. If not, the domain names are passed through unchanged.
+
+#### url.hash
+
+* {string}
+
+Gets and sets the fragment portion of the URL.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://example.org/foo#bar');
+console.log(myURL.hash);
+// Prints #bar
+
+myURL.hash = 'baz';
+console.log(myURL.href);
+// Prints https://example.org/foo#baz
+```
+
+Invalid URL characters included in the value assigned to the `hash` property
+are [percent-encoded][]. Note that the selection of which characters to
+percent-encode may vary somewhat from what the [`url.parse()`][] and
+[`url.format()`][] methods would produce.
+
+#### url.host
+
+* {string}
+
+Gets and sets the host portion of the URL.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://example.org:81/foo');
+console.log(myURL.host);
+// Prints example.org:81
+
+myURL.host = 'example.com:82';
+console.log(myURL.href);
+// Prints https://example.com:82/foo
+```
+
+Invalid host values assigned to the `host` property are ignored.
+
+#### url.hostname
+
+* {string}
+
+Gets and sets the hostname portion of the URL. The key difference between
+`url.host` and `url.hostname` is that `url.hostname` does *not* include the
+port.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://example.org:81/foo');
+console.log(myURL.hostname);
+// Prints example.org
+
+myURL.hostname = 'example.com:82';
+console.log(myURL.href);
+// Prints https://example.com:81/foo
+```
+
+Invalid hostname values assigned to the `hostname` property are ignored.
+
+#### url.href
+
+* {string}
+
+Gets and sets the serialized URL.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://example.org/foo');
+console.log(myURL.href);
+// Prints https://example.org/foo
+
+myURL.href = 'https://example.com/bar';
+console.log(myURL.href);
+// Prints https://example.com/bar
+```
+
+Getting the value of the `href` property is equivalent to calling
+[`url.toString()`][].
+
+Setting the value of this property to a new value is equivalent to creating a
+new `URL` object using [`new URL(value)`][`new URL()`]. Each of the `URL`
+object's properties will be modified.
+
+If the value assigned to the `href` property is not a valid URL, a `TypeError`
+will be thrown.
+
+#### url.origin
+
+* {string}
+
+Gets the read-only serialization of the URL's origin.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://example.org/foo/bar?baz');
+console.log(myURL.origin);
+// Prints https://example.org
+```
+
+```js
+const { URL } = require('url');
+const idnURL = new URL('https://你好你好');
+console.log(idnURL.origin);
+// Prints https://xn--6qqa088eba
+
+console.log(idnURL.hostname);
+// Prints xn--6qqa088eba
+```
+
+#### url.password
+
+* {string}
+
+Gets and sets the password portion of the URL.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://abc:xyz@example.com');
+console.log(myURL.password);
+// Prints xyz
+
+myURL.password = '123';
+console.log(myURL.href);
+// Prints https://abc:123@example.com
+```
+
+Invalid URL characters included in the value assigned to the `password` property
+are [percent-encoded][]. Note that the selection of which characters to
+percent-encode may vary somewhat from what the [`url.parse()`][] and
+[`url.format()`][] methods would produce.
+
+#### url.pathname
+
+* {string}
+
+Gets and sets the path portion of the URL.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://example.org/abc/xyz?123');
+console.log(myURL.pathname);
+// Prints /abc/xyz
+
+myURL.pathname = '/abcdef';
+console.log(myURL.href);
+// Prints https://example.org/abcdef?123
+```
+
+Invalid URL characters included in the value assigned to the `pathname`
+property are [percent-encoded][]. Note that the selection of which characters
+to percent-encode may vary somewhat from what the [`url.parse()`][] and
+[`url.format()`][] methods would produce.
+
+#### url.port
+
+* {string}
+
+Gets and sets the port portion of the URL.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://example.org:8888');
+console.log(myURL.port);
+// Prints 8888
+
+// Default ports are automatically transformed to the empty string
+// (HTTPS protocol's default port is 443)
+myURL.port = '443';
+console.log(myURL.port);
+// Prints the empty string
+console.log(myURL.href);
+// Prints https://example.org/
+
+myURL.port = 1234;
+console.log(myURL.port);
+// Prints 1234
+console.log(myURL.href);
+// Prints https://example.org:1234/
+
+// Completely invalid port strings are ignored
+myURL.port = 'abcd';
+console.log(myURL.port);
+// Prints 1234
+
+// Leading numbers are treated as a port number
+myURL.port = '5678abcd';
+console.log(myURL.port);
+// Prints 5678
+
+// Non-integers are truncated
+myURL.port = 1234.5678;
+console.log(myURL.port);
+// Prints 1234
+
+// Out-of-range numbers are ignored
+myURL.port = 1e10;
+console.log(myURL.port);
+// Prints 1234
+```
+
+The port value may be set as either a number or as a String containing a number
+in the range `0` to `65535` (inclusive). Setting the value to the default port
+of the `URL` objects given `protocol` will result in the `port` value becoming
+the empty string (`''`).
+
+If an invalid string is assigned to the `port` property, but it begins with a
+number, the leading number is assigned to `port`. Otherwise, or if the number
+lies outside the range denoted above, it is ignored.
+
+#### url.protocol
+
+* {string}
+
+Gets and sets the protocol portion of the URL.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://example.org');
+console.log(myURL.protocol);
+// Prints https:
+
+myURL.protocol = 'ftp';
+console.log(myURL.href);
+// Prints ftp://example.org/
+```
+
+Invalid URL protocol values assigned to the `protocol` property are ignored.
+
+#### url.search
+
+* {string}
+
+Gets and sets the serialized query portion of the URL.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://example.org/abc?123');
+console.log(myURL.search);
+// Prints ?123
+
+myURL.search = 'abc=xyz';
+console.log(myURL.href);
+// Prints https://example.org/abc?abc=xyz
+```
+
+Any invalid URL characters appearing in the value assigned the `search`
+property will be [percent-encoded][]. Note that the selection of which
+characters to percent-encode may vary somewhat from what the [`url.parse()`][]
+and [`url.format()`][] methods would produce.
+
+#### url.searchParams
+
+* {URLSearchParams}
+
+Gets the [`URLSearchParams`][] object representing the query parameters of the
+URL. This property is read-only; to replace the entirety of query parameters of
+the URL, use the [`url.search`][] setter. See [`URLSearchParams`][]
+documentation for details.
+
+#### url.username
+
+* {string}
+
+Gets and sets the username portion of the URL.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://abc:xyz@example.com');
+console.log(myURL.username);
+// Prints abc
+
+myURL.username = '123';
+console.log(myURL.href);
+// Prints https://123:xyz@example.com/
+```
+
+Any invalid URL characters appearing in the value assigned the `username`
+property will be [percent-encoded][]. Note that the selection of which
+characters to percent-encode may vary somewhat from what the [`url.parse()`][]
+and [`url.format()`][] methods would produce.
+
+#### url.toString()
+
+* Returns: {string}
+
+The `toString()` method on the `URL` object returns the serialized URL. The
+value returned is equivalent to that of [`url.href`][] and [`url.toJSON()`][].
+
+Because of the need for standard compliance, this method does not allow users
+to customize the serialization process of the URL.
+
+#### url.toJSON()
+
+* Returns: {string}
+
+The `toJSON()` method on the `URL` object returns the serialized URL. The
+value returned is equivalent to that of [`url.href`][] and
+[`url.toString()`][].
+
+This method is automatically called when an `URL` object is serialized
+with [`JSON.stringify()`][].
+
+```js
+const { URL } = require('url');
+const myURLs = [
+  new URL('https://www.example.com'),
+  new URL('https://test.example.org')
+];
+console.log(JSON.stringify(myURLs));
+// Prints ["https://www.example.com/","https://test.example.org/"]
+```
+
+### Class: URLSearchParams
+<!-- YAML
+added: v7.5.0, REPLACEME
+-->
+
+The `URLSearchParams` API provides read and write access to the query of a
+`URL`. The `URLSearchParams` class can also be used standalone with one of the
+four following constructors.
+
+The WHATWG `URLSearchParams` interface and the [`querystring`][] module have
+similar purpose, but the purpose of the [`querystring`][] module is more
+general, as it allows the customization of delimiter characters (`&` and `=`).
+On the other hand, this API is designed purely for URL query strings.
+
+```js
+const { URL, URLSearchParams } = require('url');
+
+const myURL = new URL('https://example.org/?abc=123');
+console.log(myURL.searchParams.get('abc'));
+// Prints 123
+
+myURL.searchParams.append('abc', 'xyz');
+console.log(myURL.href);
+// Prints https://example.org/?abc=123&abc=xyz
+
+myURL.searchParams.delete('abc');
+myURL.searchParams.set('a', 'b');
+console.log(myURL.href);
+// Prints https://example.org/?a=b
+
+const newSearchParams = new URLSearchParams(myURL.searchParams);
+// The above is equivalent to
+// const newSearchParams = new URLSearchParams(myURL.search);
+
+newSearchParams.append('a', 'c');
+console.log(myURL.href);
+// Prints https://example.org/?a=b
+console.log(newSearchParams.toString());
+// Prints a=b&a=c
+
+// newSearchParams.toString() is implicitly called
+myURL.search = newSearchParams;
+console.log(myURL.href);
+// Prints https://example.org/?a=b&a=c
+newSearchParams.delete('a');
+console.log(myURL.href);
+// Prints https://example.org/?a=b&a=c
+```
+
+#### Constructor: new URLSearchParams()
+
+Instantiate a new empty `URLSearchParams` object.
+
+#### Constructor: new URLSearchParams(string)
+
+* `string` {string} A query string
+
+Parse the `string` as a query string, and use it to instantiate a new
+`URLSearchParams` object. A leading `'?'`, if present, is ignored.
+
+```js
+const { URLSearchParams } = require('url');
+let params;
+
+params = new URLSearchParams('user=abc&query=xyz');
+console.log(params.get('user'));
+// Prints 'abc'
+console.log(params.toString());
+// Prints 'user=abc&query=xyz'
+
+params = new URLSearchParams('?user=abc&query=xyz');
+console.log(params.toString());
+// Prints 'user=abc&query=xyz'
+```
+
+#### Constructor: new URLSearchParams(obj)
+<!-- YAML
+added: v7.10.0, REPLACEME
+-->
+
+* `obj` {Object} An object representing a collection of key-value pairs
+
+Instantiate a new `URLSearchParams` object with a query hash map. The key and
+value of each property of `obj` are always coerced to strings.
+
+*Note*: Unlike [`querystring`][] module, duplicate keys in the form of array
+values are not allowed. Arrays are stringified using [`array.toString()`][],
+which simply joins all array elements with commas.
+
+```js
+const { URLSearchParams } = require('url');
+const params = new URLSearchParams({
+  user: 'abc',
+  query: ['first', 'second']
+});
+console.log(params.getAll('query'));
+// Prints [ 'first,second' ]
+console.log(params.toString());
+// Prints 'user=abc&query=first%2Csecond'
+```
+
+#### Constructor: new URLSearchParams(iterable)
+<!-- YAML
+added: v7.10.0, REPLACEME
+-->
+
+* `iterable` {Iterable} An iterable object whose elements are key-value pairs
+
+Instantiate a new `URLSearchParams` object with an iterable map in a way that
+is similar to [`Map`][]'s constructor. `iterable` can be an Array or any
+iterable object. That means `iterable` can be another `URLSearchParams`, in
+which case the constructor will simply create a clone of the provided
+`URLSearchParams`.  Elements of `iterable` are key-value pairs, and can
+themselves be any iterable object.
+
+Duplicate keys are allowed.
+
+```js
+const { URLSearchParams } = require('url');
+let params;
+
+// Using an array
+params = new URLSearchParams([
+  ['user', 'abc'],
+  ['query', 'first'],
+  ['query', 'second']
+]);
+console.log(params.toString());
+// Prints 'user=abc&query=first&query=second'
+
+// Using a Map object
+const map = new Map();
+map.set('user', 'abc');
+map.set('query', 'xyz');
+params = new URLSearchParams(map);
+console.log(params.toString());
+// Prints 'user=abc&query=xyz'
+
+// Using a generator function
+function* getQueryPairs() {
+  yield ['user', 'abc'];
+  yield ['query', 'first'];
+  yield ['query', 'second'];
+}
+params = new URLSearchParams(getQueryPairs());
+console.log(params.toString());
+// Prints 'user=abc&query=first&query=second'
+
+// Each key-value pair must have exactly two elements
+new URLSearchParams([
+  ['user', 'abc', 'error']
+]);
+// Throws TypeError [ERR_INVALID_TUPLE]:
+//        Each query pair must be an iterable [name, value] tuple
+```
+
+#### urlSearchParams.append(name, value)
+
+* `name` {string}
+* `value` {string}
+
+Append a new name-value pair to the query string.
+
+#### urlSearchParams.delete(name)
+
+* `name` {string}
+
+Remove all name-value pairs whose name is `name`.
+
+#### urlSearchParams.entries()
+
+* Returns: {Iterator}
+
+Returns an ES6 Iterator over each of the name-value pairs in the query.
+Each item of the iterator is a JavaScript Array. The first item of the Array
+is the `name`, the second item of the Array is the `value`.
+
+Alias for [`urlSearchParams[@@iterator]()`][`urlSearchParams@@iterator()`].
+
+#### urlSearchParams.forEach(fn[, thisArg])
+
+* `fn` {Function} Function invoked for each name-value pair in the query.
+* `thisArg` {Object} Object to be used as `this` value for when `fn` is called
+
+Iterates over each name-value pair in the query and invokes the given function.
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://example.org/?a=b&c=d');
+myURL.searchParams.forEach((value, name, searchParams) => {
+  console.log(name, value, myURL.searchParams === searchParams);
+});
+// Prints:
+//   a b true
+//   c d true
+```
+
+#### urlSearchParams.get(name)
+
+* `name` {string}
+* Returns: {string} or `null` if there is no name-value pair with the given
+  `name`.
+
+Returns the value of the first name-value pair whose name is `name`. If there
+are no such pairs, `null` is returned.
+
+#### urlSearchParams.getAll(name)
+
+* `name` {string}
+* Returns: {Array}
+
+Returns the values of all name-value pairs whose name is `name`. If there are
+no such pairs, an empty array is returned.
+
+#### urlSearchParams.has(name)
+
+* `name` {string}
+* Returns: {boolean}
+
+Returns `true` if there is at least one name-value pair whose name is `name`.
+
+#### urlSearchParams.keys()
+
+* Returns: {Iterator}
+
+Returns an ES6 Iterator over the names of each name-value pair.
+
+```js
+const { URLSearchParams } = require('url');
+const params = new URLSearchParams('foo=bar&foo=baz');
+for (const name of params.keys()) {
+  console.log(name);
+}
+// Prints:
+//   foo
+//   foo
+```
+
+#### urlSearchParams.set(name, value)
+
+* `name` {string}
+* `value` {string}
+
+Sets the value in the `URLSearchParams` object associated with `name` to
+`value`. If there are any pre-existing name-value pairs whose names are `name`,
+set the first such pair's value to `value` and remove all others. If not,
+append the name-value pair to the query string.
+
+```js
+const { URLSearchParams } = require('url');
+
+const params = new URLSearchParams();
+params.append('foo', 'bar');
+params.append('foo', 'baz');
+params.append('abc', 'def');
+console.log(params.toString());
+// Prints foo=bar&foo=baz&abc=def
+
+params.set('foo', 'def');
+params.set('xyz', 'opq');
+console.log(params.toString());
+// Prints foo=def&abc=def&xyz=opq
+```
+
+#### urlSearchParams.sort()
+<!-- YAML
+added: v7.7.0, REPLACEME
+-->
+
+Sort all existing name-value pairs in-place by their names. Sorting is done
+with a [stable sorting algorithm][], so relative order between name-value pairs
+with the same name is preserved.
+
+This method can be used, in particular, to increase cache hits.
+
+```js
+const { URLSearchParams } = require('url');
+const params = new URLSearchParams('query[]=abc&type=search&query[]=123');
+params.sort();
+console.log(params.toString());
+// Prints query%5B%5D=abc&query%5B%5D=123&type=search
+```
+
+#### urlSearchParams.toString()
+
+* Returns: {string}
+
+Returns the search parameters serialized as a string, with characters
+percent-encoded where necessary.
+
+#### urlSearchParams.values()
+
+* Returns: {Iterator}
+
+Returns an ES6 Iterator over the values of each name-value pair.
+
+#### urlSearchParams\[@@iterator\]()
+
+* Returns: {Iterator}
 
-For example: `'host.com:8080'`
+Returns an ES6 Iterator over each of the name-value pairs in the query string.
+Each item of the iterator is a JavaScript Array. The first item of the Array
+is the `name`, the second item of the Array is the `value`.
 
-### urlObject.auth
+Alias for [`urlSearchParams.entries()`][].
+
+```js
+const { URLSearchParams } = require('url');
+const params = new URLSearchParams('foo=bar&xyz=baz');
+for (const [name, value] of params) {
+  console.log(name, value);
+}
+// Prints:
+//   foo bar
+//   xyz baz
+```
+
+### url.domainToASCII(domain)
+<!-- YAML
+added: v7.4.0, REPLACEME
+-->
+
+* `domain` {string}
+* Returns: {string}
+
+Returns the [Punycode][] ASCII serialization of the `domain`. If `domain` is an
+invalid domain, the empty string is returned.
+
+It performs the inverse operation to [`url.domainToUnicode()`][].
+
+```js
+const url = require('url');
+console.log(url.domainToASCII('español.com'));
+// Prints xn--espaol-zwa.com
+console.log(url.domainToASCII('中文.com'));
+// Prints xn--fiq228c.com
+console.log(url.domainToASCII('xn--iñvalid.com'));
+// Prints an empty string
+```
+
+### url.domainToUnicode(domain)
+<!-- YAML
+added: v7.4.0, REPLACEME
+-->
+
+* `domain` {string}
+* Returns: {string}
+
+Returns the Unicode serialization of the `domain`. If `domain` is an invalid
+domain, the empty string is returned.
+
+It performs the inverse operation to [`url.domainToASCII()`][].
+
+```js
+const url = require('url');
+console.log(url.domainToUnicode('xn--espaol-zwa.com'));
+// Prints español.com
+console.log(url.domainToUnicode('xn--fiq228c.com'));
+// Prints 中文.com
+console.log(url.domainToUnicode('xn--iñvalid.com'));
+// Prints an empty string
+```
+
+## Legacy URL API
+
+### Legacy urlObject
+
+The legacy urlObject (`require('url').Url`) is created and returned by the
+`url.parse()` function.
+
+#### urlObject.auth
 
 The `auth` property is the username and password portion of the URL, also
 referred to as "userinfo". This string subset follows the `protocol` and
@@ -72,12 +823,33 @@ with the `[:{password}]` portion being optional.
 
 For example: `'user:pass'`
 
-### urlObject.hostname
+#### urlObject.hash
+
+The `hash` property consists of the "fragment" portion of the URL including
+the leading ASCII hash (`#`) character.
+
+For example: `'#hash'`
+
+#### urlObject.host
+
+The `host` property is the full lower-cased host portion of the URL, including
+the `port` if specified.
+
+For example: `'sub.host.com:8080'`
+
+#### urlObject.hostname
 
 The `hostname` property is the lower-cased host name portion of the `host`
 component *without* the `port` included.
 
-For example: `'host.com'`
+For example: `'sub.host.com'`
+
+#### urlObject.href
+
+The `href` property is the full URL string that was parsed with both the
+`protocol` and `host` components converted to lower-case.
+
+For example: `'http://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash'`
 
 ### urlObject.port
 
@@ -126,14 +898,22 @@ For example: `'query=string'` or `{'query': 'string'}`
 If returned as a string, no decoding of the query string is performed. If
 returned as an object, both keys and values are decoded.
 
-### urlObject.hash
+#### urlObject.search
 
-The `hash` property consists of the "fragment" portion of the URL including
-the leading ASCII hash (`#`) character.
+The `search` property consists of the entire "query string" portion of the
+URL, including the leading ASCII question mark (`?`) character.
 
-For example: `'#hash'`
+For example: `'?query=string'`
 
-## url.format(urlObject)
+No decoding of the query string is performed.
+
+#### urlObject.slashes
+
+The `slashes` property is a `boolean` with a value of `true` if two ASCII
+forward-slash characters (`/`) are required following the colon in the
+`protocol`.
+
+### url.format(urlObject)
 <!-- YAML
 added: v0.1.25
 -->
@@ -145,7 +925,7 @@ added: v0.1.25
 The `url.format()` method returns a formatted URL string derived from
 `urlObject`.
 
-If `urlObject` is not an object or a string, `url.parse()` will throw a
+If `urlObject` is not an object or a string, `url.format()` will throw a
 [`TypeError`][].
 
 The formatting process operates as follows:
@@ -201,7 +981,7 @@ The formatting process operates as follows:
 * `result` is returned.
 
 
-## url.parse(urlString[, parseQueryString[, slashesDenoteHost]])
+### url.parse(urlString[, parseQueryString[, slashesDenoteHost]])
 <!-- YAML
 added: v0.1.25
 -->
@@ -220,7 +1000,7 @@ added: v0.1.25
 The `url.parse()` method takes a URL string, parses it, and returns a URL
 object.
 
-## url.resolve(from, to)
+### url.resolve(from, to)
 <!-- YAML
 added: v0.1.25
 -->
@@ -239,11 +1019,18 @@ url.resolve('http://example.com/', '/one');    // 'http://example.com/one'
 url.resolve('http://example.com/one', '/two'); // 'http://example.com/two'
 ```
 
-## Escaped Characters
+<a id="whatwg-percent-encoding"></a>
+## Percent-Encoding in URLs
+
+URLs are permitted to only contain a certain range of characters. Any character
+falling outside of that range must be encoded. How such characters are encoded,
+and which characters to encode depends entirely on where the character is
+located within the structure of the URL.
 
-URLs are only permitted to contain a certain range of characters. Spaces (`' '`)
-and the following characters will be automatically escaped in the
-properties of URL objects:
+### Legacy API
+
+Within the Legacy API, spaces (`' '`) and the following characters will be
+automatically escaped in the properties of URL objects:
 
 ```txt
 < > " ` \r \n \t { } | \ ^ '
@@ -252,6 +1039,68 @@ properties of URL objects:
 For example, the ASCII space character (`' '`) is encoded as `%20`. The ASCII
 forward slash (`/`) character is encoded as `%3C`.
 
+### WHATWG API
+
+The [WHATWG URL Standard][] uses a more selective and fine grained approach to
+selecting encoded characters than that used by the Legacy API.
+
+The WHATWG algorithm defines three "percent-encode sets" that describe ranges
+of characters that must be percent-encoded:
+
+* The *C0 control percent-encode set* includes code points in range U+0000 to
+  U+001F (inclusive) and all code points greater than U+007E.
+
+* The *path percent-encode set* includes the *C0 control percent-encode set*
+  and code points U+0020, U+0022, U+0023, U+003C, U+003E, U+003F, U+0060,
+  U+007B, and U+007D.
+
+* The *userinfo encode set* includes the *path percent-encode set* and code
+  points U+002F, U+003A, U+003B, U+003D, U+0040, U+005B, U+005C, U+005D,
+  U+005E, and U+007C.
+
+The *userinfo percent-encode set* is used exclusively for username and
+passwords encoded within the URL. The *path percent-encode set* is used for the
+path of most URLs. The *C0 control percent-encode set* is used for all
+other cases, including URL fragments in particular, but also host and path
+under certain specific conditions.
+
+When non-ASCII characters appear within a hostname, the hostname is encoded
+using the [Punycode][] algorithm. Note, however, that a hostname *may* contain
+*both* Punycode encoded and percent-encoded characters. For example:
+
+```js
+const { URL } = require('url');
+const myURL = new URL('https://%CF%80.com/foo');
+console.log(myURL.href);
+// Prints https://xn--1xa.com/foo
+console.log(myURL.origin);
+// Prints https://π.com
+```
+
 [`Error`]: errors.html#errors_class_error
-[`querystring`]: querystring.html
+[`JSON.stringify()`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
+[`Map`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
 [`TypeError`]: errors.html#errors_class_typeerror
+[`URLSearchParams`]: #url_class_urlsearchparams
+[`array.toString()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString
+[`new URL()`]: #url_constructor_new_url_input_base
+[`querystring`]: querystring.html
+[`require('url').format()`]: #url_url_format_url_options
+[`url.domainToASCII()`]: #url_url_domaintoascii_domain
+[`url.domainToUnicode()`]: #url_url_domaintounicode_domain
+[`url.format()`]: #url_url_format_urlobject
+[`url.href`]: #url_url_href
+[`url.parse()`]: #url_url_parse_urlstring_parsequerystring_slashesdenotehost
+[`url.search`]: #url_url_search
+[`url.toJSON()`]: #url_url_tojson
+[`url.toString()`]: #url_url_tostring
+[`urlSearchParams.entries()`]: #url_urlsearchparams_entries
+[`urlSearchParams@@iterator()`]: #url_urlsearchparams_iterator
+[ICU]: intl.html#intl_options_for_building_node_js
+[Punycode]: https://tools.ietf.org/html/rfc5891#section-4.4
+[WHATWG URL Standard]: https://url.spec.whatwg.org/
+[WHATWG URL]: #url_the_whatwg_url_api
+[examples of parsed URLs]: https://url.spec.whatwg.org/#example-url-parsing
+[legacy urlObject]: #url_legacy_urlobject
+[percent-encoded]: #whatwg-percent-encoding
+[stable sorting algorithm]: https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js
index 310dc9dd029375..4b5a500adc598e 100644
--- a/lib/internal/bootstrap_node.js
+++ b/lib/internal/bootstrap_node.js
@@ -62,6 +62,10 @@
 
     _process.setupRawDebug();
 
+    // Ensure setURLConstructor() is called before the native
+    // URL::ToObject() method is used.
+    NativeModule.require('internal/url');
+
     Object.defineProperty(process, 'argv0', {
       enumerable: true,
       configurable: false,
diff --git a/lib/internal/querystring.js b/lib/internal/querystring.js
new file mode 100644
index 00000000000000..d1684418097100
--- /dev/null
+++ b/lib/internal/querystring.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const hexTable = new Array(256);
+for (var i = 0; i < 256; ++i)
+  hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
+
+const isHexTable = [
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 32 - 47
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
+  0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64 - 79
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 - 95
+  0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96 - 111
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 112 - 127
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128 ...
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0  // ... 256
+];
+
+module.exports = {
+  hexTable,
+  isHexTable
+};
diff --git a/lib/internal/url.js b/lib/internal/url.js
new file mode 100644
index 00000000000000..91a03ec99a87fd
--- /dev/null
+++ b/lib/internal/url.js
@@ -0,0 +1,1428 @@
+'use strict';
+
+const util = require('util');
+const {
+  hexTable,
+  isHexTable
+} = require('internal/querystring');
+
+const { getConstructorOf } = require('internal/util');
+const querystring = require('querystring');
+
+const { platform } = process;
+const isWindows = platform === 'win32';
+
+const {
+  domainToASCII: _domainToASCII,
+  domainToUnicode: _domainToUnicode,
+  encodeAuth,
+  toUSVString: _toUSVString,
+  parse: _parse,
+  setURLConstructor,
+  URL_FLAGS_CANNOT_BE_BASE,
+  URL_FLAGS_HAS_FRAGMENT,
+  URL_FLAGS_HAS_HOST,
+  URL_FLAGS_HAS_PASSWORD,
+  URL_FLAGS_HAS_PATH,
+  URL_FLAGS_HAS_QUERY,
+  URL_FLAGS_HAS_USERNAME,
+  URL_FLAGS_SPECIAL,
+  kFragment,
+  kHost,
+  kHostname,
+  kPathStart,
+  kPort,
+  kQuery,
+  kSchemeStart
+} = process.binding('url');
+
+const context = Symbol('context');
+const cannotBeBase = Symbol('cannot-be-base');
+const cannotHaveUsernamePasswordPort =
+    Symbol('cannot-have-username-password-port');
+const special = Symbol('special');
+const searchParams = Symbol('query');
+const kFormat = Symbol('format');
+
+// https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object
+const IteratorPrototype = Object.getPrototypeOf(
+  Object.getPrototypeOf([][Symbol.iterator]())
+);
+
+const unpairedSurrogateRe =
+    /(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/;
+function toUSVString(val) {
+  const str = `${val}`;
+  // As of V8 5.5, `str.search()` (and `unpairedSurrogateRe[@@search]()`) are
+  // slower than `unpairedSurrogateRe.exec()`.
+  const match = unpairedSurrogateRe.exec(str);
+  if (!match)
+    return str;
+  return _toUSVString(str, match.index);
+}
+
+// Refs: https://html.spec.whatwg.org/multipage/browsers.html#concept-origin-opaque
+const kOpaqueOrigin = 'null';
+
+// Refs: https://html.spec.whatwg.org/multipage/browsers.html#ascii-serialisation-of-an-origin
+function serializeTupleOrigin(scheme, host, port) {
+  return `${scheme}//${host}${port === null ? '' : `:${port}`}`;
+}
+
+// This class provides the internal state of a URL object. An instance of this
+// class is stored in every URL object and is accessed internally by setters
+// and getters. It roughly corresponds to the concept of a URL record in the
+// URL Standard, with a few differences. It is also the object transported to
+// the C++ binding.
+// Refs: https://url.spec.whatwg.org/#concept-url
+class URLContext {
+  constructor() {
+    this.flags = 0;
+    this.scheme = ':';
+    this.username = '';
+    this.password = '';
+    this.host = null;
+    this.port = null;
+    this.path = [];
+    this.query = null;
+    this.fragment = null;
+  }
+}
+
+class URLSearchParams {
+  // URL Standard says the default value is '', but as undefined and '' have
+  // the same result, undefined is used to prevent unnecessary parsing.
+  // Default parameter is necessary to keep URLSearchParams.length === 0 in
+  // accordance with Web IDL spec.
+  constructor(init = undefined) {
+    if (init === null || init === undefined) {
+      this[searchParams] = [];
+    } else if ((typeof init === 'object' && init !== null) ||
+               typeof init === 'function') {
+      const method = init[Symbol.iterator];
+      if (method === this[Symbol.iterator]) {
+        // While the spec does not have this branch, we can use it as a
+        // shortcut to avoid having to go through the costly generic iterator.
+        const childParams = init[searchParams];
+        this[searchParams] = childParams.slice();
+      } else if (method !== null && method !== undefined) {
+        if (typeof method !== 'function') {
+          throw new TypeError('Query pairs must be iterable');
+        }
+
+        // sequence<sequence<USVString>>
+        // Note: per spec we have to first exhaust the lists then process them
+        const pairs = [];
+        for (const pair of init) {
+          if ((typeof pair !== 'object' && typeof pair !== 'function') ||
+              pair === null ||
+              typeof pair[Symbol.iterator] !== 'function') {
+            throw new TypeError(
+              'Each query pair must be an iterable [name, value] tuple');
+          }
+          const convertedPair = [];
+          for (const element of pair)
+            convertedPair.push(toUSVString(element));
+          pairs.push(convertedPair);
+        }
+
+        this[searchParams] = [];
+        for (const pair of pairs) {
+          if (pair.length !== 2) {
+            throw new TypeError(
+              'Each query pair must be an iterable [name, value] tuple');
+          }
+          this[searchParams].push(pair[0], pair[1]);
+        }
+      } else {
+        // record<USVString, USVString>
+        // Need to use reflection APIs for full spec compliance.
+        this[searchParams] = [];
+        const keys = Reflect.ownKeys(init);
+        for (var i = 0; i < keys.length; i++) {
+          const key = keys[i];
+          const desc = Reflect.getOwnPropertyDescriptor(init, key);
+          if (desc !== undefined && desc.enumerable) {
+            const typedKey = toUSVString(key);
+            const typedValue = toUSVString(init[key]);
+            this[searchParams].push(typedKey, typedValue);
+          }
+        }
+      }
+    } else {
+      // USVString
+      init = toUSVString(init);
+      if (init[0] === '?') init = init.slice(1);
+      initSearchParams(this, init);
+    }
+
+    // "associated url object"
+    this[context] = null;
+  }
+
+  [util.inspect.custom](recurseTimes, ctx) {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+
+    if (typeof recurseTimes === 'number' && recurseTimes < 0)
+      return ctx.stylize('[Object]', 'special');
+
+    var separator = ', ';
+    var innerOpts = util._extend({}, ctx);
+    if (recurseTimes !== null) {
+      innerOpts.depth = recurseTimes - 1;
+    }
+    var innerInspect = (v) => util.inspect(v, innerOpts);
+
+    var list = this[searchParams];
+    var output = [];
+    for (var i = 0; i < list.length; i += 2)
+      output.push(`${innerInspect(list[i])} => ${innerInspect(list[i + 1])}`);
+
+    var colorRe = /\u001b\[\d\d?m/g;
+    var length = output.reduce(
+      (prev, cur) => prev + cur.replace(colorRe, '').length + separator.length,
+      -separator.length
+    );
+    if (length > ctx.breakLength) {
+      return `${this.constructor.name} {\n  ${output.join(',\n  ')} }`;
+    } else if (output.length) {
+      return `${this.constructor.name} { ${output.join(separator)} }`;
+    } else {
+      return `${this.constructor.name} {}`;
+    }
+  }
+}
+
+function onParseComplete(flags, protocol, username, password,
+                         host, port, path, query, fragment) {
+  var ctx = this[context];
+  ctx.flags = flags;
+  ctx.scheme = protocol;
+  ctx.username = (flags & URL_FLAGS_HAS_USERNAME) !== 0 ? username : '';
+  ctx.password = (flags & URL_FLAGS_HAS_PASSWORD) !== 0 ? password : '';
+  ctx.port = port;
+  ctx.path = (flags & URL_FLAGS_HAS_PATH) !== 0 ? path : [];
+  ctx.query = query;
+  ctx.fragment = fragment;
+  ctx.host = host;
+  if (!this[searchParams]) { // invoked from URL constructor
+    this[searchParams] = new URLSearchParams();
+    this[searchParams][context] = this;
+  }
+  initSearchParams(this[searchParams], query);
+}
+
+function onParseError(flags, input) {
+  const error = new TypeError(`Invalid URL: ${input}`);
+  error.input = input;
+  throw error;
+}
+
+// Reused by URL constructor and URL#href setter.
+function parse(url, input, base) {
+  const base_context = base ? base[context] : undefined;
+  url[context] = new URLContext();
+  _parse(input.trim(), -1, base_context, undefined,
+         onParseComplete.bind(url), onParseError);
+}
+
+function onParseProtocolComplete(flags, protocol, username, password,
+                                 host, port, path, query, fragment) {
+  const ctx = this[context];
+  if ((flags & URL_FLAGS_SPECIAL) !== 0) {
+    ctx.flags |= URL_FLAGS_SPECIAL;
+  } else {
+    ctx.flags &= ~URL_FLAGS_SPECIAL;
+  }
+  ctx.scheme = protocol;
+  ctx.port = port;
+}
+
+function onParseHostComplete(flags, protocol, username, password,
+                             host, port, path, query, fragment) {
+  const ctx = this[context];
+  if ((flags & URL_FLAGS_HAS_HOST) !== 0) {
+    ctx.host = host;
+    ctx.flags |= URL_FLAGS_HAS_HOST;
+  } else {
+    ctx.host = null;
+    ctx.flags &= ~URL_FLAGS_HAS_HOST;
+  }
+  if (port !== null)
+    ctx.port = port;
+}
+
+function onParseHostnameComplete(flags, protocol, username, password,
+                                 host, port, path, query, fragment) {
+  const ctx = this[context];
+  if ((flags & URL_FLAGS_HAS_HOST) !== 0) {
+    ctx.host = host;
+    ctx.flags |= URL_FLAGS_HAS_HOST;
+  } else {
+    ctx.host = null;
+    ctx.flags &= ~URL_FLAGS_HAS_HOST;
+  }
+}
+
+function onParsePortComplete(flags, protocol, username, password,
+                             host, port, path, query, fragment) {
+  this[context].port = port;
+}
+
+function onParsePathComplete(flags, protocol, username, password,
+                             host, port, path, query, fragment) {
+  const ctx = this[context];
+  if ((flags & URL_FLAGS_HAS_PATH) !== 0) {
+    ctx.path = path;
+    ctx.flags |= URL_FLAGS_HAS_PATH;
+  } else {
+    ctx.path = [];
+    ctx.flags &= ~URL_FLAGS_HAS_PATH;
+  }
+
+  // The C++ binding may set host to empty string.
+  if ((flags & URL_FLAGS_HAS_HOST) !== 0) {
+    ctx.host = host;
+    ctx.flags |= URL_FLAGS_HAS_HOST;
+  }
+}
+
+function onParseSearchComplete(flags, protocol, username, password,
+                               host, port, path, query, fragment) {
+  this[context].query = query;
+}
+
+function onParseHashComplete(flags, protocol, username, password,
+                             host, port, path, query, fragment) {
+  this[context].fragment = fragment;
+}
+
+class URL {
+  constructor(input, base) {
+    // toUSVString is not needed.
+    input = `${input}`;
+    if (base !== undefined &&
+        (!base[searchParams] || !base[searchParams][searchParams])) {
+      base = new URL(base);
+    }
+    parse(this, input, base);
+  }
+
+  get [special]() {
+    return (this[context].flags & URL_FLAGS_SPECIAL) !== 0;
+  }
+
+  get [cannotBeBase]() {
+    return (this[context].flags & URL_FLAGS_CANNOT_BE_BASE) !== 0;
+  }
+
+  // https://url.spec.whatwg.org/#cannot-have-a-username-password-port
+  get [cannotHaveUsernamePasswordPort]() {
+    const { host, scheme } = this[context];
+    return ((host == null || host === '') ||
+            this[cannotBeBase] ||
+            scheme === 'file:');
+  }
+
+  [util.inspect.custom](depth, opts) {
+    if (this == null ||
+        Object.getPrototypeOf(this[context]) !== URLContext.prototype) {
+      throw new TypeError('Value of "this" must be of type URL');
+    }
+
+    if (typeof depth === 'number' && depth < 0)
+      return opts.stylize('[Object]', 'special');
+
+    var ctor = getConstructorOf(this);
+
+    var obj = Object.create({
+      constructor: ctor === null ? URL : ctor
+    });
+
+    obj.href = this.href;
+    obj.origin = this.origin;
+    obj.protocol = this.protocol;
+    obj.username = this.username;
+    obj.password = this.password;
+    obj.host = this.host;
+    obj.hostname = this.hostname;
+    obj.port = this.port;
+    obj.pathname = this.pathname;
+    obj.search = this.search;
+    obj.searchParams = this.searchParams;
+    obj.hash = this.hash;
+
+    if (opts.showHidden) {
+      obj.cannotBeBase = this[cannotBeBase];
+      obj.special = this[special];
+      obj[context] = this[context];
+    }
+
+    return util.inspect(obj, opts);
+  }
+}
+
+Object.defineProperties(URL.prototype, {
+  [kFormat]: {
+    enumerable: false,
+    configurable: false,
+    // eslint-disable-next-line func-name-matching
+    value: function format(options) {
+      if (options && typeof options !== 'object')
+        throw new TypeError('The "options" argument must be of type Object');
+      options = util._extend({
+        fragment: true,
+        unicode: false,
+        search: true,
+        auth: true
+      }, options);
+      const ctx = this[context];
+      var ret = ctx.scheme;
+      if (ctx.host !== null) {
+        ret += '//';
+        const has_username = ctx.username !== '';
+        const has_password = ctx.password !== '';
+        if (options.auth && (has_username || has_password)) {
+          if (has_username)
+            ret += ctx.username;
+          if (has_password)
+            ret += `:${ctx.password}`;
+          ret += '@';
+        }
+        ret += options.unicode ?
+          domainToUnicode(this.host) : this.host;
+      } else if (ctx.scheme === 'file:') {
+        ret += '//';
+      }
+      if (this.pathname)
+        ret += this.pathname;
+      if (options.search && ctx.query !== null)
+        ret += `?${ctx.query}`;
+      if (options.fragment && ctx.fragment !== null)
+        ret += `#${ctx.fragment}`;
+      return ret;
+    }
+  },
+  [Symbol.toStringTag]: {
+    configurable: true,
+    value: 'URL'
+  },
+  toString: {
+    // https://heycam.github.io/webidl/#es-stringifier
+    writable: true,
+    enumerable: true,
+    configurable: true,
+    // eslint-disable-next-line func-name-matching
+    value: function toString() {
+      return this[kFormat]({});
+    }
+  },
+  href: {
+    enumerable: true,
+    configurable: true,
+    get() {
+      return this[kFormat]({});
+    },
+    set(input) {
+      // toUSVString is not needed.
+      input = `${input}`;
+      parse(this, input);
+    }
+  },
+  origin: {  // readonly
+    enumerable: true,
+    configurable: true,
+    get() {
+      // Refs: https://url.spec.whatwg.org/#concept-url-origin
+      const ctx = this[context];
+      switch (ctx.scheme) {
+        case 'blob:':
+          if (ctx.path.length > 0) {
+            try {
+              return (new URL(ctx.path[0])).origin;
+            } catch (err) {
+              // fall through... do nothing
+            }
+          }
+          return kOpaqueOrigin;
+        case 'ftp:':
+        case 'gopher:':
+        case 'http:':
+        case 'https:':
+        case 'ws:':
+        case 'wss:':
+          return serializeTupleOrigin(ctx.scheme, ctx.host, ctx.port);
+      }
+      return kOpaqueOrigin;
+    }
+  },
+  protocol: {
+    enumerable: true,
+    configurable: true,
+    get() {
+      return this[context].scheme;
+    },
+    set(scheme) {
+      // toUSVString is not needed.
+      scheme = `${scheme}`;
+      if (scheme.length === 0)
+        return;
+      const ctx = this[context];
+      if (ctx.scheme === 'file:' &&
+          (ctx.host === '' || ctx.host === null)) {
+        return;
+      }
+      _parse(scheme, kSchemeStart, null, ctx,
+             onParseProtocolComplete.bind(this));
+    }
+  },
+  username: {
+    enumerable: true,
+    configurable: true,
+    get() {
+      return this[context].username;
+    },
+    set(username) {
+      // toUSVString is not needed.
+      username = `${username}`;
+      if (this[cannotHaveUsernamePasswordPort])
+        return;
+      const ctx = this[context];
+      if (username === '') {
+        ctx.username = '';
+        ctx.flags &= ~URL_FLAGS_HAS_USERNAME;
+        return;
+      }
+      ctx.username = encodeAuth(username);
+      ctx.flags |= URL_FLAGS_HAS_USERNAME;
+    }
+  },
+  password: {
+    enumerable: true,
+    configurable: true,
+    get() {
+      return this[context].password;
+    },
+    set(password) {
+      // toUSVString is not needed.
+      password = `${password}`;
+      if (this[cannotHaveUsernamePasswordPort])
+        return;
+      const ctx = this[context];
+      if (password === '') {
+        ctx.password = '';
+        ctx.flags &= ~URL_FLAGS_HAS_PASSWORD;
+        return;
+      }
+      ctx.password = encodeAuth(password);
+      ctx.flags |= URL_FLAGS_HAS_PASSWORD;
+    }
+  },
+  host: {
+    enumerable: true,
+    configurable: true,
+    get() {
+      const ctx = this[context];
+      var ret = ctx.host || '';
+      if (ctx.port !== null)
+        ret += `:${ctx.port}`;
+      return ret;
+    },
+    set(host) {
+      const ctx = this[context];
+      // toUSVString is not needed.
+      host = `${host}`;
+      if (this[cannotBeBase]) {
+        // Cannot set the host if cannot-be-base is set
+        return;
+      }
+      _parse(host, kHost, null, ctx, onParseHostComplete.bind(this));
+    }
+  },
+  hostname: {
+    enumerable: true,
+    configurable: true,
+    get() {
+      return this[context].host || '';
+    },
+    set(host) {
+      const ctx = this[context];
+      // toUSVString is not needed.
+      host = `${host}`;
+      if (this[cannotBeBase]) {
+        // Cannot set the host if cannot-be-base is set
+        return;
+      }
+      _parse(host, kHostname, null, ctx, onParseHostnameComplete.bind(this));
+    }
+  },
+  port: {
+    enumerable: true,
+    configurable: true,
+    get() {
+      const port = this[context].port;
+      return port === null ? '' : String(port);
+    },
+    set(port) {
+      // toUSVString is not needed.
+      port = `${port}`;
+      if (this[cannotHaveUsernamePasswordPort])
+        return;
+      const ctx = this[context];
+      if (port === '') {
+        ctx.port = null;
+        return;
+      }
+      _parse(port, kPort, null, ctx, onParsePortComplete.bind(this));
+    }
+  },
+  pathname: {
+    enumerable: true,
+    configurable: true,
+    get() {
+      const ctx = this[context];
+      if (this[cannotBeBase])
+        return ctx.path[0];
+      if (ctx.path.length === 0)
+        return '';
+      return `/${ctx.path.join('/')}`;
+    },
+    set(path) {
+      // toUSVString is not needed.
+      path = `${path}`;
+      if (this[cannotBeBase])
+        return;
+      _parse(path, kPathStart, null, this[context],
+             onParsePathComplete.bind(this));
+    }
+  },
+  search: {
+    enumerable: true,
+    configurable: true,
+    get() {
+      const { query } = this[context];
+      if (query === null || query === '')
+        return '';
+      return `?${query}`;
+    },
+    set(search) {
+      const ctx = this[context];
+      search = toUSVString(search);
+      if (search === '') {
+        ctx.query = null;
+        ctx.flags &= ~URL_FLAGS_HAS_QUERY;
+      } else {
+        if (search[0] === '?') search = search.slice(1);
+        ctx.query = '';
+        ctx.flags |= URL_FLAGS_HAS_QUERY;
+        if (search) {
+          _parse(search, kQuery, null, ctx, onParseSearchComplete.bind(this));
+        }
+      }
+      initSearchParams(this[searchParams], search);
+    }
+  },
+  searchParams: {  // readonly
+    enumerable: true,
+    configurable: true,
+    get() {
+      return this[searchParams];
+    }
+  },
+  hash: {
+    enumerable: true,
+    configurable: true,
+    get() {
+      const { fragment } = this[context];
+      if (fragment === null || fragment === '')
+        return '';
+      return `#${fragment}`;
+    },
+    set(hash) {
+      const ctx = this[context];
+      // toUSVString is not needed.
+      hash = `${hash}`;
+      if (!hash) {
+        ctx.fragment = null;
+        ctx.flags &= ~URL_FLAGS_HAS_FRAGMENT;
+        return;
+      }
+      if (hash[0] === '#') hash = hash.slice(1);
+      ctx.fragment = '';
+      ctx.flags |= URL_FLAGS_HAS_FRAGMENT;
+      _parse(hash, kFragment, null, ctx, onParseHashComplete.bind(this));
+    }
+  },
+  toJSON: {
+    writable: true,
+    enumerable: true,
+    configurable: true,
+    // eslint-disable-next-line func-name-matching
+    value: function toJSON() {
+      return this[kFormat]({});
+    }
+  }
+});
+
+function update(url, params) {
+  if (!url)
+    return;
+
+  const ctx = url[context];
+  const serializedParams = params.toString();
+  if (serializedParams) {
+    ctx.query = serializedParams;
+    ctx.flags |= URL_FLAGS_HAS_QUERY;
+  } else {
+    ctx.query = null;
+    ctx.flags &= ~URL_FLAGS_HAS_QUERY;
+  }
+}
+
+function initSearchParams(url, init) {
+  if (!init) {
+    url[searchParams] = [];
+    return;
+  }
+  url[searchParams] = parseParams(init);
+}
+
+// application/x-www-form-urlencoded parser
+// Ref: https://url.spec.whatwg.org/#concept-urlencoded-parser
+function parseParams(qs) {
+  const out = [];
+  var pairStart = 0;
+  var lastPos = 0;
+  var seenSep = false;
+  var buf = '';
+  var encoded = false;
+  var encodeCheck = 0;
+  var i;
+  for (i = 0; i < qs.length; ++i) {
+    const code = qs.charCodeAt(i);
+
+    // Try matching key/value pair separator
+    if (code === 38/*&*/) {
+      if (pairStart === i) {
+        // We saw an empty substring between pair separators
+        lastPos = pairStart = i + 1;
+        continue;
+      }
+
+      if (lastPos < i)
+        buf += qs.slice(lastPos, i);
+      if (encoded)
+        buf = querystring.unescape(buf);
+      out.push(buf);
+
+      // If `buf` is the key, add an empty value.
+      if (!seenSep)
+        out.push('');
+
+      seenSep = false;
+      buf = '';
+      encoded = false;
+      encodeCheck = 0;
+      lastPos = pairStart = i + 1;
+      continue;
+    }
+
+    // Try matching key/value separator (e.g. '=') if we haven't already
+    if (!seenSep && code === 61/*=*/) {
+      // Key/value separator match!
+      if (lastPos < i)
+        buf += qs.slice(lastPos, i);
+      if (encoded)
+        buf = querystring.unescape(buf);
+      out.push(buf);
+
+      seenSep = true;
+      buf = '';
+      encoded = false;
+      encodeCheck = 0;
+      lastPos = i + 1;
+      continue;
+    }
+
+    // Handle + and percent decoding.
+    if (code === 43/*+*/) {
+      if (lastPos < i)
+        buf += qs.slice(lastPos, i);
+      buf += ' ';
+      lastPos = i + 1;
+    } else if (!encoded) {
+      // Try to match an (valid) encoded byte (once) to minimize unnecessary
+      // calls to string decoding functions
+      if (code === 37/*%*/) {
+        encodeCheck = 1;
+      } else if (encodeCheck > 0) {
+        // eslint-disable-next-line no-extra-boolean-cast
+        if (!!isHexTable[code]) {
+          if (++encodeCheck === 3)
+            encoded = true;
+        } else {
+          encodeCheck = 0;
+        }
+      }
+    }
+  }
+
+  // Deal with any leftover key or value data
+
+  // There is a trailing &. No more processing is needed.
+  if (pairStart === i)
+    return out;
+
+  if (lastPos < i)
+    buf += qs.slice(lastPos, i);
+  if (encoded)
+    buf = querystring.unescape(buf);
+  out.push(buf);
+
+  // If `buf` is the key, add an empty value.
+  if (!seenSep)
+    out.push('');
+
+  return out;
+}
+
+// Adapted from querystring's implementation.
+// Ref: https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer
+const noEscape = [
+//0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 0x30 - 0x3F
+  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F
+  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0  // 0x70 - 0x7F
+];
+
+// Special version of hexTable that uses `+` for U+0020 SPACE.
+const paramHexTable = hexTable.slice();
+paramHexTable[0x20] = '+';
+
+function escapeParam(str) {
+  const len = str.length;
+  if (len === 0)
+    return '';
+
+  var out = '';
+  var lastPos = 0;
+
+  for (var i = 0; i < len; i++) {
+    var c = str.charCodeAt(i);
+
+    // ASCII
+    if (c < 0x80) {
+      if (noEscape[c] === 1)
+        continue;
+      if (lastPos < i)
+        out += str.slice(lastPos, i);
+      lastPos = i + 1;
+      out += paramHexTable[c];
+      continue;
+    }
+
+    if (lastPos < i)
+      out += str.slice(lastPos, i);
+
+    // Multi-byte characters ...
+    if (c < 0x800) {
+      lastPos = i + 1;
+      out += paramHexTable[0xC0 | (c >> 6)] +
+             paramHexTable[0x80 | (c & 0x3F)];
+      continue;
+    }
+    if (c < 0xD800 || c >= 0xE000) {
+      lastPos = i + 1;
+      out += paramHexTable[0xE0 | (c >> 12)] +
+             paramHexTable[0x80 | ((c >> 6) & 0x3F)] +
+             paramHexTable[0x80 | (c & 0x3F)];
+      continue;
+    }
+    // Surrogate pair
+    ++i;
+    var c2;
+    if (i < len)
+      c2 = str.charCodeAt(i) & 0x3FF;
+    else {
+      // This branch should never happen because all URLSearchParams entries
+      // should already be converted to USVString. But, included for
+      // completion's sake anyway.
+      c2 = 0;
+    }
+    lastPos = i + 1;
+    c = 0x10000 + (((c & 0x3FF) << 10) | c2);
+    out += paramHexTable[0xF0 | (c >> 18)] +
+           paramHexTable[0x80 | ((c >> 12) & 0x3F)] +
+           paramHexTable[0x80 | ((c >> 6) & 0x3F)] +
+           paramHexTable[0x80 | (c & 0x3F)];
+  }
+  if (lastPos === 0)
+    return str;
+  if (lastPos < len)
+    return out + str.slice(lastPos);
+  return out;
+}
+
+// application/x-www-form-urlencoded serializer
+// Ref: https://url.spec.whatwg.org/#concept-urlencoded-serializer
+function serializeParams(array) {
+  const len = array.length;
+  if (len === 0)
+    return '';
+
+  var output = `${escapeParam(array[0])}=${escapeParam(array[1])}`;
+  for (var i = 2; i < len; i += 2)
+    output += `&${escapeParam(array[i])}=${escapeParam(array[i + 1])}`;
+  return output;
+}
+
+// Mainly to mitigate func-name-matching ESLint rule
+function defineIDLClass(proto, classStr, obj) {
+  // https://heycam.github.io/webidl/#dfn-class-string
+  Object.defineProperty(proto, Symbol.toStringTag, {
+    writable: false,
+    enumerable: false,
+    configurable: true,
+    value: classStr
+  });
+
+  // https://heycam.github.io/webidl/#es-operations
+  for (const key of Object.keys(obj)) {
+    Object.defineProperty(proto, key, {
+      writable: true,
+      enumerable: true,
+      configurable: true,
+      value: obj[key]
+    });
+  }
+  for (const key of Object.getOwnPropertySymbols(obj)) {
+    Object.defineProperty(proto, key, {
+      writable: true,
+      enumerable: false,
+      configurable: true,
+      value: obj[key]
+    });
+  }
+}
+
+// for merge sort
+function merge(out, start, mid, end, lBuffer, rBuffer) {
+  const sizeLeft = mid - start;
+  const sizeRight = end - mid;
+  var l, r, o;
+
+  for (l = 0; l < sizeLeft; l++)
+    lBuffer[l] = out[start + l];
+  for (r = 0; r < sizeRight; r++)
+    rBuffer[r] = out[mid + r];
+
+  l = 0;
+  r = 0;
+  o = start;
+  while (l < sizeLeft && r < sizeRight) {
+    if (lBuffer[l] <= rBuffer[r]) {
+      out[o++] = lBuffer[l++];
+      out[o++] = lBuffer[l++];
+    } else {
+      out[o++] = rBuffer[r++];
+      out[o++] = rBuffer[r++];
+    }
+  }
+  while (l < sizeLeft)
+    out[o++] = lBuffer[l++];
+  while (r < sizeRight)
+    out[o++] = rBuffer[r++];
+}
+
+defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
+  append(name, value) {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+    if (arguments.length < 2) {
+      throw new TypeError('The "name" and "value" arguments must be specified');
+    }
+
+    name = toUSVString(name);
+    value = toUSVString(value);
+    this[searchParams].push(name, value);
+    update(this[context], this);
+  },
+
+  delete(name) {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+    if (arguments.length < 1) {
+      throw new TypeError('The "name" argument must be specified');
+    }
+
+    const list = this[searchParams];
+    name = toUSVString(name);
+    for (var i = 0; i < list.length;) {
+      const cur = list[i];
+      if (cur === name) {
+        list.splice(i, 2);
+      } else {
+        i += 2;
+      }
+    }
+    update(this[context], this);
+  },
+
+  get(name) {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+    if (arguments.length < 1) {
+      throw new TypeError('The "name" argument must be specified');
+    }
+
+    const list = this[searchParams];
+    name = toUSVString(name);
+    for (var i = 0; i < list.length; i += 2) {
+      if (list[i] === name) {
+        return list[i + 1];
+      }
+    }
+    return null;
+  },
+
+  getAll(name) {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+    if (arguments.length < 1) {
+      throw new TypeError('The "name" argument must be specified');
+    }
+
+    const list = this[searchParams];
+    const values = [];
+    name = toUSVString(name);
+    for (var i = 0; i < list.length; i += 2) {
+      if (list[i] === name) {
+        values.push(list[i + 1]);
+      }
+    }
+    return values;
+  },
+
+  has(name) {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+    if (arguments.length < 1) {
+      throw new TypeError('The "name" argument must be specified');
+    }
+
+    const list = this[searchParams];
+    name = toUSVString(name);
+    for (var i = 0; i < list.length; i += 2) {
+      if (list[i] === name) {
+        return true;
+      }
+    }
+    return false;
+  },
+
+  set(name, value) {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+    if (arguments.length < 2) {
+      throw new TypeError('The "name" and "value" arguments must be specified');
+    }
+
+    const list = this[searchParams];
+    name = toUSVString(name);
+    value = toUSVString(value);
+
+    // If there are any name-value pairs whose name is `name`, in `list`, set
+    // the value of the first such name-value pair to `value` and remove the
+    // others.
+    var found = false;
+    for (var i = 0; i < list.length;) {
+      const cur = list[i];
+      if (cur === name) {
+        if (!found) {
+          list[i + 1] = value;
+          found = true;
+          i += 2;
+        } else {
+          list.splice(i, 2);
+        }
+      } else {
+        i += 2;
+      }
+    }
+
+    // Otherwise, append a new name-value pair whose name is `name` and value
+    // is `value`, to `list`.
+    if (!found) {
+      list.push(name, value);
+    }
+
+    update(this[context], this);
+  },
+
+  sort() {
+    const a = this[searchParams];
+    const len = a.length;
+
+    if (len <= 2) {
+      // Nothing needs to be done.
+    } else if (len < 100) {
+      // 100 is found through testing.
+      // Simple stable in-place insertion sort
+      // Derived from v8/src/js/array.js
+      for (var i = 2; i < len; i += 2) {
+        var curKey = a[i];
+        var curVal = a[i + 1];
+        var j;
+        for (j = i - 2; j >= 0; j -= 2) {
+          if (a[j] > curKey) {
+            a[j + 2] = a[j];
+            a[j + 3] = a[j + 1];
+          } else {
+            break;
+          }
+        }
+        a[j + 2] = curKey;
+        a[j + 3] = curVal;
+      }
+    } else {
+      // Bottom-up iterative stable merge sort
+      const lBuffer = new Array(len);
+      const rBuffer = new Array(len);
+      for (var step = 2; step < len; step *= 2) {
+        for (var start = 0; start < len - 2; start += 2 * step) {
+          var mid = start + step;
+          var end = mid + step;
+          end = end < len ? end : len;
+          if (mid > end)
+            continue;
+          merge(a, start, mid, end, lBuffer, rBuffer);
+        }
+      }
+    }
+
+    update(this[context], this);
+  },
+
+  // https://heycam.github.io/webidl/#es-iterators
+  // Define entries here rather than [Symbol.iterator] as the function name
+  // must be set to `entries`.
+  entries() {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+
+    return createSearchParamsIterator(this, 'key+value');
+  },
+
+  forEach(callback, thisArg = undefined) {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+    if (typeof callback !== 'function') {
+      throw new TypeError('Callback must be a function');
+    }
+
+    let list = this[searchParams];
+
+    var i = 0;
+    while (i < list.length) {
+      const key = list[i];
+      const value = list[i + 1];
+      callback.call(thisArg, value, key, this);
+      // in case the URL object's `search` is updated
+      list = this[searchParams];
+      i += 2;
+    }
+  },
+
+  // https://heycam.github.io/webidl/#es-iterable
+  keys() {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+
+    return createSearchParamsIterator(this, 'key');
+  },
+
+  values() {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+
+    return createSearchParamsIterator(this, 'value');
+  },
+
+  // https://heycam.github.io/webidl/#es-stringifier
+  // https://url.spec.whatwg.org/#urlsearchparams-stringification-behavior
+  toString() {
+    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
+      throw new TypeError('Value of "this" must be of type URLSearchParams');
+    }
+
+    return serializeParams(this[searchParams]);
+  }
+});
+
+// https://heycam.github.io/webidl/#es-iterable-entries
+Object.defineProperty(URLSearchParams.prototype, Symbol.iterator, {
+  writable: true,
+  configurable: true,
+  value: URLSearchParams.prototype.entries
+});
+
+// https://heycam.github.io/webidl/#dfn-default-iterator-object
+function createSearchParamsIterator(target, kind) {
+  const iterator = Object.create(URLSearchParamsIteratorPrototype);
+  iterator[context] = {
+    target,
+    kind,
+    index: 0
+  };
+  return iterator;
+}
+
+// https://heycam.github.io/webidl/#dfn-iterator-prototype-object
+const URLSearchParamsIteratorPrototype = Object.create(IteratorPrototype);
+
+defineIDLClass(URLSearchParamsIteratorPrototype, 'URLSearchParamsIterator', {
+  next() {
+    if (!this ||
+        Object.getPrototypeOf(this) !== URLSearchParamsIteratorPrototype) {
+      throw new TypeError(
+        'Value of "this" must be of type URLSearchParamsIterator');
+    }
+
+    const {
+      target,
+      kind,
+      index
+    } = this[context];
+    const values = target[searchParams];
+    const len = values.length;
+    if (index >= len) {
+      return {
+        value: undefined,
+        done: true
+      };
+    }
+
+    const name = values[index];
+    const value = values[index + 1];
+    this[context].index = index + 2;
+
+    let result;
+    if (kind === 'key') {
+      result = name;
+    } else if (kind === 'value') {
+      result = value;
+    } else {
+      result = [name, value];
+    }
+
+    return {
+      value: result,
+      done: false
+    };
+  },
+  [util.inspect.custom](recurseTimes, ctx) {
+    if (this == null || this[context] == null || this[context].target == null)
+      throw new TypeError(
+        'Value of "this" must be of type URLSearchParamsIterator');
+
+    if (typeof recurseTimes === 'number' && recurseTimes < 0)
+      return ctx.stylize('[Object]', 'special');
+
+    const innerOpts = util._extend({}, ctx);
+    if (recurseTimes !== null) {
+      innerOpts.depth = recurseTimes - 1;
+    }
+    const {
+      target,
+      kind,
+      index
+    } = this[context];
+    const output = target[searchParams].slice(index).reduce((prev, cur, i) => {
+      const key = i % 2 === 0;
+      if (kind === 'key' && key) {
+        prev.push(cur);
+      } else if (kind === 'value' && !key) {
+        prev.push(cur);
+      } else if (kind === 'key+value' && !key) {
+        prev.push([target[searchParams][index + i - 1], cur]);
+      }
+      return prev;
+    }, []);
+    const breakLn = util.inspect(output, innerOpts).includes('\n');
+    const outputStrs = output.map((p) => util.inspect(p, innerOpts));
+    let outputStr;
+    if (breakLn) {
+      outputStr = `\n  ${outputStrs.join(',\n  ')}`;
+    } else {
+      outputStr = ` ${outputStrs.join(', ')}`;
+    }
+    return `${this[Symbol.toStringTag]} {${outputStr} }`;
+  }
+});
+
+function domainToASCII(domain) {
+  if (arguments.length < 1)
+    throw new TypeError('The "domain" argument must be specified');
+
+  // toUSVString is not needed.
+  return _domainToASCII(`${domain}`);
+}
+
+function domainToUnicode(domain) {
+  if (arguments.length < 1)
+    throw new TypeError('The "domain" argument must be specified');
+
+  // toUSVString is not needed.
+  return _domainToUnicode(`${domain}`);
+}
+
+// Utility function that converts a URL object into an ordinary
+// options object as expected by the http.request and https.request
+// APIs.
+function urlToOptions(url) {
+  var options = {
+    protocol: url.protocol,
+    hostname: url.hostname,
+    hash: url.hash,
+    search: url.search,
+    pathname: url.pathname,
+    path: `${url.pathname}${url.search}`,
+    href: url.href
+  };
+  if (url.port !== '') {
+    options.port = Number(url.port);
+  }
+  if (url.username || url.password) {
+    options.auth = `${url.username}:${url.password}`;
+  }
+  return options;
+}
+
+function getPathFromURLWin32(url) {
+  var hostname = url.hostname;
+  var pathname = url.pathname;
+  for (var n = 0; n < pathname.length; n++) {
+    if (pathname[n] === '%') {
+      var third = pathname.codePointAt(n + 2) | 0x20;
+      if ((pathname[n + 1] === '2' && third === 102) || // 2f 2F /
+          (pathname[n + 1] === '5' && third === 99)) {  // 5c 5C \
+        return new TypeError(
+          'File URL path must not include encoded \\ or / characters');
+      }
+    }
+  }
+  pathname = decodeURIComponent(pathname);
+  if (hostname !== '') {
+    // If hostname is set, then we have a UNC path
+    // Pass the hostname through domainToUnicode just in case
+    // it is an IDN using punycode encoding. We do not need to worry
+    // about percent encoding because the URL parser will have
+    // already taken care of that for us. Note that this only
+    // causes IDNs with an appropriate `xn--` prefix to be decoded.
+    return `//${domainToUnicode(hostname)}${pathname}`;
+  } else {
+    // Otherwise, it's a local path that requires a drive letter
+    var letter = pathname.codePointAt(1) | 0x20;
+    var sep = pathname[2];
+    if (letter < 97 || letter > 122 ||   // a..z A..Z
+        (sep !== ':')) {
+      return new TypeError('File URL path must be absolute');
+    }
+    return pathname.slice(1);
+  }
+}
+
+function getPathFromURLPosix(url) {
+  if (url.hostname !== '') {
+    return new TypeError(
+      `File URL host must be "localhost" or empty on ${platform}`);
+  }
+  var pathname = url.pathname;
+  for (var n = 0; n < pathname.length; n++) {
+    if (pathname[n] === '%') {
+      var third = pathname.codePointAt(n + 2) | 0x20;
+      if (pathname[n + 1] === '2' && third === 102) {
+        return new TypeError(
+          'File URL path must not include encoded / characters');
+      }
+    }
+  }
+  return decodeURIComponent(pathname);
+}
+
+function getPathFromURL(path) {
+  if (path == null || !path[searchParams] ||
+      !path[searchParams][searchParams]) {
+    return path;
+  }
+  if (path.protocol !== 'file:')
+    return new TypeError('The URL must be of scheme file');
+  return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
+}
+
+// We percent-encode % character when converting from file path to URL,
+// as this is the only character that won't be percent encoded by
+// default URL percent encoding when pathname is set.
+const percentRegEx = /%/g;
+function getURLFromFilePath(filepath) {
+  const tmp = new URL('file://');
+  if (filepath.includes('%'))
+    filepath = filepath.replace(percentRegEx, '%25');
+  tmp.pathname = filepath;
+  return tmp;
+}
+
+function NativeURL(ctx) {
+  this[context] = ctx;
+}
+NativeURL.prototype = URL.prototype;
+
+function constructUrl(flags, protocol, username, password,
+                      host, port, path, query, fragment) {
+  var ctx = new URLContext();
+  ctx.flags = flags;
+  ctx.scheme = protocol;
+  ctx.username = (flags & URL_FLAGS_HAS_USERNAME) !== 0 ? username : '';
+  ctx.password = (flags & URL_FLAGS_HAS_PASSWORD) !== 0 ? password : '';
+  ctx.port = port;
+  ctx.path = (flags & URL_FLAGS_HAS_PATH) !== 0 ? path : [];
+  ctx.query = query;
+  ctx.fragment = fragment;
+  ctx.host = host;
+  const url = new NativeURL(ctx);
+  url[searchParams] = new URLSearchParams();
+  url[searchParams][context] = url;
+  initSearchParams(url[searchParams], query);
+  return url;
+}
+setURLConstructor(constructUrl);
+
+module.exports = {
+  toUSVString,
+  getPathFromURL,
+  getURLFromFilePath,
+  URL,
+  URLSearchParams,
+  domainToASCII,
+  domainToUnicode,
+  urlToOptions,
+  formatSymbol: kFormat,
+  searchParamsSymbol: searchParams
+};
diff --git a/lib/internal/util.js b/lib/internal/util.js
index 58aa011c08c6b9..f66698295f33e7 100644
--- a/lib/internal/util.js
+++ b/lib/internal/util.js
@@ -6,6 +6,21 @@ const prefix = `(${process.release.name}:${process.pid}) `;
 exports.getHiddenValue = binding.getHiddenValue;
 exports.setHiddenValue = binding.setHiddenValue;
 
+exports.getConstructorOf = function getConstructorOf(obj) {
+  while (obj) {
+    var descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor');
+    if (descriptor !== undefined &&
+        typeof descriptor.value === 'function' &&
+        descriptor.value.name !== '') {
+      return descriptor.value;
+    }
+
+    obj = Object.getPrototypeOf(obj);
+  }
+
+  return null;
+};
+
 // The `buffer` module uses this. Defining it here instead of in the public
 // `util` module makes it accessible without having to `require('util')` there.
 exports.customInspectSymbol = Symbol('util.inspect.custom');
diff --git a/lib/querystring.js b/lib/querystring.js
index 5ccb5fa77b320f..668628cdc386f6 100644
--- a/lib/querystring.js
+++ b/lib/querystring.js
@@ -1,5 +1,6 @@
 'use strict';
 
+const { hexTable } = require('internal/querystring');
 const QueryString = module.exports = {
   unescapeBuffer,
   // `unescape()` is a JS global, so we need to use a different local name
@@ -116,10 +117,6 @@ function qsUnescape(s, decodeSpaces) {
 }
 
 
-const hexTable = [];
-for (var i = 0; i < 256; ++i)
-  hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
-
 // These characters do not need escaping when generating query strings:
 // ! - . _ ~
 // ' ( ) *
diff --git a/lib/url.js b/lib/url.js
index 214c8a93204e70..980a2b2a1f00f2 100644
--- a/lib/url.js
+++ b/lib/url.js
@@ -3,12 +3,17 @@
 const { toASCII } = process.binding('config').hasIntl ?
   process.binding('icu') : require('punycode');
 
-exports.parse = urlParse;
-exports.resolve = urlResolve;
-exports.resolveObject = urlResolveObject;
-exports.format = urlFormat;
+const { hexTable } = require('internal/querystring');
 
-exports.Url = Url;
+// WHATWG URL implementation provided by internal/url
+const {
+  URL,
+  URLSearchParams,
+  domainToASCII,
+  domainToUnicode
+} = require('internal/url');
+
+// Original url.parse() API
 
 function Url() {
   this.protocol = null;
@@ -311,7 +316,10 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
       // It only converts parts of the domain name that
       // have non-ASCII characters, i.e. it doesn't matter if
       // you call it with a domain that already is ASCII-only.
-      this.hostname = toASCII(this.hostname);
+
+      // Use lenient mode (`true`) to try to support even non-compliant
+      // URLs.
+      this.hostname = toASCII(this.hostname, true);
     }
 
     var p = this.port ? ':' + this.port : '';
@@ -939,10 +947,6 @@ function spliceOne(list, index) {
   list.pop();
 }
 
-var hexTable = new Array(256);
-for (var i = 0; i < 256; ++i)
-  hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
-
 // These characters do not need escaping:
 // ! - . _ ~
 // ' ( ) * :
@@ -1014,3 +1018,18 @@ function encodeAuth(str) {
     return out + str.slice(lastPos);
   return out;
 }
+
+module.exports = {
+  // Original API
+  Url,
+  parse: urlParse,
+  resolve: urlResolve,
+  resolveObject: urlResolveObject,
+  format: urlFormat,
+
+  // WHATWG API
+  URL,
+  URLSearchParams,
+  domainToASCII,
+  domainToUnicode
+};
diff --git a/lib/util.js b/lib/util.js
index 349186ad48b172..441f2364631f3e 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -263,22 +263,6 @@ function arrayToHash(array) {
 }
 
 
-function getConstructorOf(obj) {
-  while (obj) {
-    var descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor');
-    if (descriptor !== undefined &&
-        typeof descriptor.value === 'function' &&
-        descriptor.value.name !== '') {
-      return descriptor.value;
-    }
-
-    obj = Object.getPrototypeOf(obj);
-  }
-
-  return null;
-}
-
-
 function ensureDebugIsInitialized() {
   if (Debug === undefined) {
     const runInDebugContext = require('vm').runInDebugContext;
@@ -438,12 +422,12 @@ function formatValue(ctx, value, recurseTimes) {
     // Can't do the same for DataView because it has a non-primitive
     // .buffer property that we need to recurse for.
     if (binding.isArrayBuffer(value) || binding.isSharedArrayBuffer(value)) {
-      return `${getConstructorOf(value).name}` +
+      return `${internalUtil.getConstructorOf(value).name}` +
              ` { byteLength: ${formatNumber(ctx, value.byteLength)} }`;
     }
   }
 
-  var constructor = getConstructorOf(value);
+  var constructor = internalUtil.getConstructorOf(value);
   var base = '', empty = false, braces;
   var formatter = formatObject;
 
diff --git a/node.gyp b/node.gyp
index 345188257998e0..fd0838b0f014eb 100644
--- a/node.gyp
+++ b/node.gyp
@@ -85,9 +85,11 @@
       'lib/internal/process/stdio.js',
       'lib/internal/process/warning.js',
       'lib/internal/process.js',
+      'lib/internal/querystring.js',
       'lib/internal/readline.js',
       'lib/internal/repl.js',
       'lib/internal/socket_list.js',
+      'lib/internal/url.js',
       'lib/internal/util.js',
       'lib/internal/v8_prof_polyfill.js',
       'lib/internal/v8_prof_processor.js',
@@ -161,6 +163,7 @@
         'src/node_main.cc',
         'src/node_os.cc',
         'src/node_revert.cc',
+        'src/node_url.cc',
         'src/node_util.cc',
         'src/node_v8.cc',
         'src/node_stat_watcher.cc',
@@ -587,6 +590,7 @@
         '<(OBJ_PATH)/node.<(OBJ_SUFFIX)',
         '<(OBJ_PATH)/node_buffer.<(OBJ_SUFFIX)',
         '<(OBJ_PATH)/node_i18n.<(OBJ_SUFFIX)',
+        '<(OBJ_PATH)/node_url.<(OBJ_SUFFIX)',
         '<(OBJ_PATH)/debug-agent.<(OBJ_SUFFIX)',
         '<(OBJ_PATH)/util.<(OBJ_SUFFIX)',
         '<(OBJ_PATH)/string_bytes.<(OBJ_SUFFIX)',
@@ -610,6 +614,7 @@
       'sources': [
         'test/cctest/test_base64.cc',
         'test/cctest/test_util.cc',
+        'test/cctest/test_url.cc'
       ],
 
       'sources!': [
diff --git a/src/env.h b/src/env.h
index 940c97fe4ddbf1..c4489857161b75 100644
--- a/src/env.h
+++ b/src/env.h
@@ -252,6 +252,7 @@ namespace node {
   V(tls_wrap_constructor_template, v8::FunctionTemplate)                      \
   V(tty_constructor_template, v8::FunctionTemplate)                           \
   V(udp_constructor_function, v8::Function)                                   \
+  V(url_constructor_function, v8::Function)                                   \
   V(write_wrap_constructor_function, v8::Function)                            \
 
 class Environment;
diff --git a/src/node_i18n.cc b/src/node_i18n.cc
index 8e32f1fe08177b..c013027981755f 100644
--- a/src/node_i18n.cc
+++ b/src/node_i18n.cc
@@ -77,12 +77,11 @@ bool InitializeICUDirectory(const std::string& path) {
   }
 }
 
-static int32_t ToUnicode(MaybeStackBuffer<char>* buf,
-                         const char* input,
-                         size_t length) {
+int32_t ToUnicode(MaybeStackBuffer<char>* buf,
+                  const char* input,
+                  size_t length) {
   UErrorCode status = U_ZERO_ERROR;
-  uint32_t options = UIDNA_DEFAULT;
-  options |= UIDNA_NONTRANSITIONAL_TO_UNICODE;
+  uint32_t options = UIDNA_NONTRANSITIONAL_TO_UNICODE;
   UIDNA* uidna = uidna_openUTS46(options, &status);
   if (U_FAILURE(status))
     return -1;
@@ -90,33 +89,52 @@ static int32_t ToUnicode(MaybeStackBuffer<char>* buf,
 
   int32_t len = uidna_nameToUnicodeUTF8(uidna,
                                         input, length,
-                                        **buf, buf->length(),
+                                        **buf, buf->capacity(),
                                         &info,
                                         &status);
 
+  // Do not check info.errors like we do with ToASCII since ToUnicode always
+  // returns a string, despite any possible errors that may have occurred.
+
   if (status == U_BUFFER_OVERFLOW_ERROR) {
     status = U_ZERO_ERROR;
     buf->AllocateSufficientStorage(len);
     len = uidna_nameToUnicodeUTF8(uidna,
                                   input, length,
-                                  **buf, buf->length(),
+                                  **buf, buf->capacity(),
                                   &info,
                                   &status);
   }
 
-  if (U_FAILURE(status))
+  // info.errors is ignored as UTS #46 ToUnicode always produces a Unicode
+  // string, regardless of whether an error occurred.
+
+  if (U_FAILURE(status)) {
     len = -1;
+    buf->SetLength(0);
+  } else {
+    buf->SetLength(len);
+  }
 
   uidna_close(uidna);
   return len;
 }
 
-static int32_t ToASCII(MaybeStackBuffer<char>* buf,
-                       const char* input,
-                       size_t length) {
+int32_t ToASCII(MaybeStackBuffer<char>* buf,
+                const char* input,
+                size_t length,
+                enum idna_mode mode) {
   UErrorCode status = U_ZERO_ERROR;
-  uint32_t options = UIDNA_DEFAULT;
-  options |= UIDNA_NONTRANSITIONAL_TO_ASCII;
+  uint32_t options =                  // CheckHyphens = false; handled later
+    UIDNA_CHECK_BIDI |                // CheckBidi = true
+    UIDNA_CHECK_CONTEXTJ |            // CheckJoiners = true
+    UIDNA_NONTRANSITIONAL_TO_ASCII;   // Nontransitional_Processing
+  if (mode == IDNA_STRICT) {
+    options |= UIDNA_USE_STD3_RULES;  // UseSTD3ASCIIRules = beStrict
+                                      // VerifyDnsLength = beStrict;
+                                      //   handled later
+  }
+
   UIDNA* uidna = uidna_openUTS46(options, &status);
   if (U_FAILURE(status))
     return -1;
@@ -124,7 +142,7 @@ static int32_t ToASCII(MaybeStackBuffer<char>* buf,
 
   int32_t len = uidna_nameToASCII_UTF8(uidna,
                                        input, length,
-                                       **buf, buf->length(),
+                                       **buf, buf->capacity(),
                                        &info,
                                        &status);
 
@@ -133,13 +151,45 @@ static int32_t ToASCII(MaybeStackBuffer<char>* buf,
     buf->AllocateSufficientStorage(len);
     len = uidna_nameToASCII_UTF8(uidna,
                                  input, length,
-                                 **buf, buf->length(),
+                                 **buf, buf->capacity(),
                                  &info,
                                  &status);
   }
 
-  if (U_FAILURE(status))
+  // In UTS #46 which specifies ToASCII, certain error conditions are
+  // configurable through options, and the WHATWG URL Standard promptly elects
+  // to disable some of them to accommodate for real-world use cases.
+  // Unfortunately, ICU4C's IDNA module does not support disabling some of
+  // these options through `options` above, and thus continues throwing
+  // unnecessary errors. To counter this situation, we just filter out the
+  // errors that may have happened afterwards, before deciding whether to
+  // return an error from this function.
+
+  // CheckHyphens = false
+  // (Specified in the current UTS #46 draft rev. 18.)
+  // Refs:
+  // - https://github.com/whatwg/url/issues/53
+  // - https://github.com/whatwg/url/pull/309
+  // - http://www.unicode.org/review/pri317/
+  // - http://www.unicode.org/reports/tr46/tr46-18.html
+  // - https://www.icann.org/news/announcement-2000-01-07-en
+  info.errors &= ~UIDNA_ERROR_HYPHEN_3_4;
+  info.errors &= ~UIDNA_ERROR_LEADING_HYPHEN;
+  info.errors &= ~UIDNA_ERROR_TRAILING_HYPHEN;
+
+  if (mode != IDNA_STRICT) {
+    // VerifyDnsLength = beStrict
+    info.errors &= ~UIDNA_ERROR_EMPTY_LABEL;
+    info.errors &= ~UIDNA_ERROR_LABEL_TOO_LONG;
+    info.errors &= ~UIDNA_ERROR_DOMAIN_NAME_TOO_LONG;
+  }
+
+  if (U_FAILURE(status) || (mode != IDNA_LENIENT && info.errors != 0)) {
     len = -1;
+    buf->SetLength(0);
+  } else {
+    buf->SetLength(len);
+  }
 
   uidna_close(uidna);
   return len;
@@ -169,8 +219,12 @@ static void ToASCII(const FunctionCallbackInfo<Value>& args) {
   CHECK_GE(args.Length(), 1);
   CHECK(args[0]->IsString());
   Utf8Value val(env->isolate(), args[0]);
+  // optional arg
+  bool lenient = args[1]->BooleanValue(env->context()).FromJust();
+  enum idna_mode mode = lenient ? IDNA_LENIENT : IDNA_DEFAULT;
+
   MaybeStackBuffer<char> buf;
-  int32_t len = ToASCII(&buf, *val, val.length());
+  int32_t len = ToASCII(&buf, *val, val.length(), mode);
 
   if (len < 0) {
     return env->ThrowError("Cannot convert name to ASCII");
diff --git a/src/node_i18n.h b/src/node_i18n.h
index ff9e87cea7fe83..30acefd1a8a996 100644
--- a/src/node_i18n.h
+++ b/src/node_i18n.h
@@ -16,6 +16,30 @@ namespace i18n {
 
 bool InitializeICUDirectory(const std::string& path);
 
+enum idna_mode {
+  // Default mode for maximum compatibility.
+  IDNA_DEFAULT,
+  // Ignore all errors in IDNA conversion, if possible.
+  IDNA_LENIENT,
+  // Enforce STD3 rules (UseSTD3ASCIIRules) and DNS length restrictions
+  // (VerifyDnsLength). Corresponds to `beStrict` flag in the "domain to ASCII"
+  // algorithm.
+  IDNA_STRICT
+};
+
+// Implements the WHATWG URL Standard "domain to ASCII" algorithm.
+// https://url.spec.whatwg.org/#concept-domain-to-ascii
+int32_t ToASCII(MaybeStackBuffer<char>* buf,
+                const char* input,
+                size_t length,
+                enum idna_mode mode = IDNA_DEFAULT);
+
+// Implements the WHATWG URL Standard "domain to Unicode" algorithm.
+// https://url.spec.whatwg.org/#concept-domain-to-unicode
+int32_t ToUnicode(MaybeStackBuffer<char>* buf,
+                  const char* input,
+                  size_t length);
+
 }  // namespace i18n
 }  // namespace node
 
diff --git a/src/node_url.cc b/src/node_url.cc
new file mode 100644
index 00000000000000..49c005a23bc2c7
--- /dev/null
+++ b/src/node_url.cc
@@ -0,0 +1,2223 @@
+#include "node_url.h"
+#include "node_internals.h"
+#include "base-object-inl.h"
+#include "node_i18n.h"
+
+#include <string>
+#include <vector>
+#include <stdio.h>
+#include <cmath>
+
+namespace node {
+
+using v8::Array;
+using v8::Context;
+using v8::Function;
+using v8::FunctionCallbackInfo;
+using v8::HandleScope;
+using v8::Integer;
+using v8::Isolate;
+using v8::Local;
+using v8::MaybeLocal;
+using v8::Null;
+using v8::Object;
+using v8::String;
+using v8::TryCatch;
+using v8::Undefined;
+using v8::Value;
+
+#define GET(env, obj, name)                                                   \
+  obj->Get(env->context(),                                                    \
+           OneByteString(env->isolate(), name)).ToLocalChecked()
+
+#define GET_AND_SET(env, obj, name, data, flag)                               \
+  {                                                                           \
+    Local<Value> val = GET(env, obj, #name);                                  \
+    if (val->IsString()) {                                                    \
+      Utf8Value value(env->isolate(), val.As<String>());                      \
+      data->name = *value;                                                    \
+      data->flags |= flag;                                                    \
+    }                                                                         \
+  }
+
+#define UTF8STRING(isolate, str)                                              \
+  String::NewFromUtf8(isolate, str.c_str(), v8::NewStringType::kNormal)       \
+    .ToLocalChecked()
+
+namespace url {
+
+// https://url.spec.whatwg.org/#eof-code-point
+static const char kEOL = -1;
+
+// Used in ToUSVString().
+static const char16_t kUnicodeReplacementCharacter = 0xFFFD;
+
+// https://url.spec.whatwg.org/#concept-host
+union url_host_value {
+  std::string domain;
+  uint32_t ipv4;
+  uint16_t ipv6[8];
+  std::string opaque;
+  ~url_host_value() {}
+};
+
+enum url_host_type {
+  HOST_TYPE_FAILED = -1,
+  HOST_TYPE_DOMAIN = 0,
+  HOST_TYPE_IPV4 = 1,
+  HOST_TYPE_IPV6 = 2,
+  HOST_TYPE_OPAQUE = 3,
+};
+
+struct url_host {
+  url_host_value value;
+  enum url_host_type type;
+};
+
+#define ARGS(XX)                                                              \
+  XX(ARG_FLAGS)                                                               \
+  XX(ARG_PROTOCOL)                                                            \
+  XX(ARG_USERNAME)                                                            \
+  XX(ARG_PASSWORD)                                                            \
+  XX(ARG_HOST)                                                                \
+  XX(ARG_PORT)                                                                \
+  XX(ARG_PATH)                                                                \
+  XX(ARG_QUERY)                                                               \
+  XX(ARG_FRAGMENT)
+
+#define ERR_ARGS(XX)                                                          \
+  XX(ERR_ARG_FLAGS)                                                           \
+  XX(ERR_ARG_INPUT)                                                           \
+
+enum url_cb_args {
+#define XX(name) name,
+  ARGS(XX)
+#undef XX
+};
+
+enum url_error_cb_args {
+#define XX(name) name,
+  ERR_ARGS(XX)
+#undef XX
+};
+
+#define CHAR_TEST(bits, name, expr)                                           \
+  template <typename T>                                                       \
+  static inline bool name(const T ch) {                                       \
+    static_assert(sizeof(ch) >= (bits) / 8,                                   \
+                  "Character must be wider than " #bits " bits");             \
+    return (expr);                                                            \
+  }
+
+#define TWO_CHAR_STRING_TEST(bits, name, expr)                                \
+  template <typename T>                                                       \
+  static inline bool name(const T ch1, const T ch2) {                         \
+    static_assert(sizeof(ch1) >= (bits) / 8,                                  \
+                  "Character must be wider than " #bits " bits");             \
+    return (expr);                                                            \
+  }                                                                           \
+  template <typename T>                                                       \
+  static inline bool name(const std::basic_string<T>& str) {                  \
+    static_assert(sizeof(str[0]) >= (bits) / 8,                               \
+                  "Character must be wider than " #bits " bits");             \
+    return str.length() >= 2 && name(str[0], str[1]);                         \
+  }
+
+// https://infra.spec.whatwg.org/#ascii-tab-or-newline
+CHAR_TEST(8, IsASCIITabOrNewline, (ch == '\t' || ch == '\n' || ch == '\r'))
+
+// https://infra.spec.whatwg.org/#c0-control-or-space
+CHAR_TEST(8, IsC0ControlOrSpace, (ch >= '\0' && ch <= ' '))
+
+// https://infra.spec.whatwg.org/#ascii-digit
+CHAR_TEST(8, IsASCIIDigit, (ch >= '0' && ch <= '9'))
+
+// https://infra.spec.whatwg.org/#ascii-hex-digit
+CHAR_TEST(8, IsASCIIHexDigit, (IsASCIIDigit(ch) ||
+                               (ch >= 'A' && ch <= 'F') ||
+                               (ch >= 'a' && ch <= 'f')))
+
+// https://infra.spec.whatwg.org/#ascii-alpha
+CHAR_TEST(8, IsASCIIAlpha, ((ch >= 'A' && ch <= 'Z') ||
+                            (ch >= 'a' && ch <= 'z')))
+
+// https://infra.spec.whatwg.org/#ascii-alphanumeric
+CHAR_TEST(8, IsASCIIAlphanumeric, (IsASCIIDigit(ch) || IsASCIIAlpha(ch)))
+
+// https://infra.spec.whatwg.org/#ascii-lowercase
+template <typename T>
+static inline T ASCIILowercase(T ch) {
+  return IsASCIIAlpha(ch) ? (ch | 0x20) : ch;
+}
+
+// https://url.spec.whatwg.org/#forbidden-host-code-point
+CHAR_TEST(8, IsForbiddenHostCodePoint,
+          ch == '\0' || ch == '\t' || ch == '\n' || ch == '\r' ||
+          ch == ' ' || ch == '#' || ch == '%' || ch == '/' ||
+          ch == ':' || ch == '?' || ch == '@' || ch == '[' ||
+          ch == '\\' || ch == ']')
+
+// https://url.spec.whatwg.org/#windows-drive-letter
+TWO_CHAR_STRING_TEST(8, IsWindowsDriveLetter,
+                     (IsASCIIAlpha(ch1) && (ch2 == ':' || ch2 == '|')))
+
+// https://url.spec.whatwg.org/#normalized-windows-drive-letter
+TWO_CHAR_STRING_TEST(8, IsNormalizedWindowsDriveLetter,
+                     (IsASCIIAlpha(ch1) && ch2 == ':'))
+
+// If a UTF-16 character is a low/trailing surrogate.
+CHAR_TEST(16, IsUnicodeTrail, (ch & 0xFC00) == 0xDC00)
+
+// If a UTF-16 character is a surrogate.
+CHAR_TEST(16, IsUnicodeSurrogate, (ch & 0xF800) == 0xD800)
+
+// If a UTF-16 surrogate is a low/trailing one.
+CHAR_TEST(16, IsUnicodeSurrogateTrail, (ch & 0x400) != 0)
+
+#undef CHAR_TEST
+#undef TWO_CHAR_STRING_TEST
+
+static const char* hex[256] = {
+  "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
+  "%08", "%09", "%0A", "%0B", "%0C", "%0D", "%0E", "%0F",
+  "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
+  "%18", "%19", "%1A", "%1B", "%1C", "%1D", "%1E", "%1F",
+  "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27",
+  "%28", "%29", "%2A", "%2B", "%2C", "%2D", "%2E", "%2F",
+  "%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37",
+  "%38", "%39", "%3A", "%3B", "%3C", "%3D", "%3E", "%3F",
+  "%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47",
+  "%48", "%49", "%4A", "%4B", "%4C", "%4D", "%4E", "%4F",
+  "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57",
+  "%58", "%59", "%5A", "%5B", "%5C", "%5D", "%5E", "%5F",
+  "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67",
+  "%68", "%69", "%6A", "%6B", "%6C", "%6D", "%6E", "%6F",
+  "%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77",
+  "%78", "%79", "%7A", "%7B", "%7C", "%7D", "%7E", "%7F",
+  "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
+  "%88", "%89", "%8A", "%8B", "%8C", "%8D", "%8E", "%8F",
+  "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
+  "%98", "%99", "%9A", "%9B", "%9C", "%9D", "%9E", "%9F",
+  "%A0", "%A1", "%A2", "%A3", "%A4", "%A5", "%A6", "%A7",
+  "%A8", "%A9", "%AA", "%AB", "%AC", "%AD", "%AE", "%AF",
+  "%B0", "%B1", "%B2", "%B3", "%B4", "%B5", "%B6", "%B7",
+  "%B8", "%B9", "%BA", "%BB", "%BC", "%BD", "%BE", "%BF",
+  "%C0", "%C1", "%C2", "%C3", "%C4", "%C5", "%C6", "%C7",
+  "%C8", "%C9", "%CA", "%CB", "%CC", "%CD", "%CE", "%CF",
+  "%D0", "%D1", "%D2", "%D3", "%D4", "%D5", "%D6", "%D7",
+  "%D8", "%D9", "%DA", "%DB", "%DC", "%DD", "%DE", "%DF",
+  "%E0", "%E1", "%E2", "%E3", "%E4", "%E5", "%E6", "%E7",
+  "%E8", "%E9", "%EA", "%EB", "%EC", "%ED", "%EE", "%EF",
+  "%F0", "%F1", "%F2", "%F3", "%F4", "%F5", "%F6", "%F7",
+  "%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF"
+};
+
+static const uint8_t C0_CONTROL_ENCODE_SET[32] = {
+  // 00     01     02     03     04     05     06     07
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 08     09     0A     0B     0C     0D     0E     0F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 10     11     12     13     14     15     16     17
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 18     19     1A     1B     1C     1D     1E     1F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 20     21     22     23     24     25     26     27
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 28     29     2A     2B     2C     2D     2E     2F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 30     31     32     33     34     35     36     37
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 38     39     3A     3B     3C     3D     3E     3F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 40     41     42     43     44     45     46     47
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 48     49     4A     4B     4C     4D     4E     4F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 50     51     52     53     54     55     56     57
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 58     59     5A     5B     5C     5D     5E     5F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 60     61     62     63     64     65     66     67
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 68     69     6A     6B     6C     6D     6E     6F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 70     71     72     73     74     75     76     77
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 78     79     7A     7B     7C     7D     7E     7F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x80,
+  // 80     81     82     83     84     85     86     87
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 88     89     8A     8B     8C     8D     8E     8F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 90     91     92     93     94     95     96     97
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 98     99     9A     9B     9C     9D     9E     9F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // A0     A1     A2     A3     A4     A5     A6     A7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // A8     A9     AA     AB     AC     AD     AE     AF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // B0     B1     B2     B3     B4     B5     B6     B7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // B8     B9     BA     BB     BC     BD     BE     BF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // C0     C1     C2     C3     C4     C5     C6     C7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // C8     C9     CA     CB     CC     CD     CE     CF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // D0     D1     D2     D3     D4     D5     D6     D7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // D8     D9     DA     DB     DC     DD     DE     DF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // E0     E1     E2     E3     E4     E5     E6     E7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // E8     E9     EA     EB     EC     ED     EE     EF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // F0     F1     F2     F3     F4     F5     F6     F7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // F8     F9     FA     FB     FC     FD     FE     FF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80
+};
+
+static const uint8_t PATH_ENCODE_SET[32] = {
+  // 00     01     02     03     04     05     06     07
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 08     09     0A     0B     0C     0D     0E     0F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 10     11     12     13     14     15     16     17
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 18     19     1A     1B     1C     1D     1E     1F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 20     21     22     23     24     25     26     27
+    0x01 | 0x00 | 0x04 | 0x08 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 28     29     2A     2B     2C     2D     2E     2F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 30     31     32     33     34     35     36     37
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 38     39     3A     3B     3C     3D     3E     3F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x10 | 0x00 | 0x40 | 0x80,
+  // 40     41     42     43     44     45     46     47
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 48     49     4A     4B     4C     4D     4E     4F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 50     51     52     53     54     55     56     57
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 58     59     5A     5B     5C     5D     5E     5F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 60     61     62     63     64     65     66     67
+    0x01 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 68     69     6A     6B     6C     6D     6E     6F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 70     71     72     73     74     75     76     77
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 78     79     7A     7B     7C     7D     7E     7F
+    0x00 | 0x00 | 0x00 | 0x08 | 0x00 | 0x20 | 0x00 | 0x80,
+  // 80     81     82     83     84     85     86     87
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 88     89     8A     8B     8C     8D     8E     8F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 90     91     92     93     94     95     96     97
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 98     99     9A     9B     9C     9D     9E     9F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // A0     A1     A2     A3     A4     A5     A6     A7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // A8     A9     AA     AB     AC     AD     AE     AF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // B0     B1     B2     B3     B4     B5     B6     B7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // B8     B9     BA     BB     BC     BD     BE     BF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // C0     C1     C2     C3     C4     C5     C6     C7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // C8     C9     CA     CB     CC     CD     CE     CF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // D0     D1     D2     D3     D4     D5     D6     D7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // D8     D9     DA     DB     DC     DD     DE     DF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // E0     E1     E2     E3     E4     E5     E6     E7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // E8     E9     EA     EB     EC     ED     EE     EF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // F0     F1     F2     F3     F4     F5     F6     F7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // F8     F9     FA     FB     FC     FD     FE     FF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80
+};
+
+static const uint8_t USERINFO_ENCODE_SET[32] = {
+  // 00     01     02     03     04     05     06     07
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 08     09     0A     0B     0C     0D     0E     0F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 10     11     12     13     14     15     16     17
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 18     19     1A     1B     1C     1D     1E     1F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 20     21     22     23     24     25     26     27
+    0x01 | 0x00 | 0x04 | 0x08 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 28     29     2A     2B     2C     2D     2E     2F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x80,
+  // 30     31     32     33     34     35     36     37
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 38     39     3A     3B     3C     3D     3E     3F
+    0x00 | 0x00 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 40     41     42     43     44     45     46     47
+    0x01 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 48     49     4A     4B     4C     4D     4E     4F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 50     51     52     53     54     55     56     57
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 58     59     5A     5B     5C     5D     5E     5F
+    0x00 | 0x00 | 0x00 | 0x08 | 0x10 | 0x20 | 0x40 | 0x00,
+  // 60     61     62     63     64     65     66     67
+    0x01 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 68     69     6A     6B     6C     6D     6E     6F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 70     71     72     73     74     75     76     77
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 78     79     7A     7B     7C     7D     7E     7F
+    0x00 | 0x00 | 0x00 | 0x08 | 0x10 | 0x20 | 0x00 | 0x80,
+  // 80     81     82     83     84     85     86     87
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 88     89     8A     8B     8C     8D     8E     8F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 90     91     92     93     94     95     96     97
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 98     99     9A     9B     9C     9D     9E     9F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // A0     A1     A2     A3     A4     A5     A6     A7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // A8     A9     AA     AB     AC     AD     AE     AF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // B0     B1     B2     B3     B4     B5     B6     B7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // B8     B9     BA     BB     BC     BD     BE     BF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // C0     C1     C2     C3     C4     C5     C6     C7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // C8     C9     CA     CB     CC     CD     CE     CF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // D0     D1     D2     D3     D4     D5     D6     D7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // D8     D9     DA     DB     DC     DD     DE     DF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // E0     E1     E2     E3     E4     E5     E6     E7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // E8     E9     EA     EB     EC     ED     EE     EF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // F0     F1     F2     F3     F4     F5     F6     F7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // F8     F9     FA     FB     FC     FD     FE     FF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80
+};
+
+static const uint8_t QUERY_ENCODE_SET[32] = {
+  // 00     01     02     03     04     05     06     07
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 08     09     0A     0B     0C     0D     0E     0F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 10     11     12     13     14     15     16     17
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 18     19     1A     1B     1C     1D     1E     1F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 20     21     22     23     24     25     26     27
+    0x01 | 0x00 | 0x04 | 0x08 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 28     29     2A     2B     2C     2D     2E     2F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 30     31     32     33     34     35     36     37
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 38     39     3A     3B     3C     3D     3E     3F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x10 | 0x00 | 0x40 | 0x00,
+  // 40     41     42     43     44     45     46     47
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 48     49     4A     4B     4C     4D     4E     4F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 50     51     52     53     54     55     56     57
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 58     59     5A     5B     5C     5D     5E     5F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 60     61     62     63     64     65     66     67
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 68     69     6A     6B     6C     6D     6E     6F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 70     71     72     73     74     75     76     77
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00,
+  // 78     79     7A     7B     7C     7D     7E     7F
+    0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x80,
+  // 80     81     82     83     84     85     86     87
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 88     89     8A     8B     8C     8D     8E     8F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 90     91     92     93     94     95     96     97
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // 98     99     9A     9B     9C     9D     9E     9F
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // A0     A1     A2     A3     A4     A5     A6     A7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // A8     A9     AA     AB     AC     AD     AE     AF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // B0     B1     B2     B3     B4     B5     B6     B7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // B8     B9     BA     BB     BC     BD     BE     BF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // C0     C1     C2     C3     C4     C5     C6     C7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // C8     C9     CA     CB     CC     CD     CE     CF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // D0     D1     D2     D3     D4     D5     D6     D7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // D8     D9     DA     DB     DC     DD     DE     DF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // E0     E1     E2     E3     E4     E5     E6     E7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // E8     E9     EA     EB     EC     ED     EE     EF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // F0     F1     F2     F3     F4     F5     F6     F7
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
+  // F8     F9     FA     FB     FC     FD     FE     FF
+    0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80
+};
+
+static inline bool BitAt(const uint8_t a[], const uint8_t i) {
+  return !!(a[i >> 3] & (1 << (i & 7)));
+}
+
+// Appends ch to str. If ch position in encode_set is set, the ch will
+// be percent-encoded then appended.
+static inline void AppendOrEscape(std::string* str,
+                                  const unsigned char ch,
+                                  const uint8_t encode_set[]) {
+  if (BitAt(encode_set, ch))
+    *str += hex[ch];
+  else
+    *str += ch;
+}
+
+template <typename T>
+static inline unsigned hex2bin(const T ch) {
+  if (ch >= '0' && ch <= '9')
+    return ch - '0';
+  if (ch >= 'A' && ch <= 'F')
+    return 10 + (ch - 'A');
+  if (ch >= 'a' && ch <= 'f')
+    return 10 + (ch - 'a');
+  return static_cast<unsigned>(-1);
+}
+
+static inline void PercentDecode(const char* input,
+                                 size_t len,
+                                 std::string* dest) {
+  if (len == 0)
+    return;
+  dest->reserve(len);
+  const char* pointer = input;
+  const char* end = input + len;
+
+  while (pointer < end) {
+    const char ch = pointer[0];
+    const size_t remaining = end - pointer - 1;
+    if (ch != '%' || remaining < 2 ||
+        (ch == '%' &&
+         (!IsASCIIHexDigit(pointer[1]) ||
+          !IsASCIIHexDigit(pointer[2])))) {
+      *dest += ch;
+      pointer++;
+      continue;
+    } else {
+      unsigned a = hex2bin(pointer[1]);
+      unsigned b = hex2bin(pointer[2]);
+      char c = static_cast<char>(a * 16 + b);
+      *dest += c;
+      pointer += 3;
+    }
+  }
+}
+
+#define SPECIALS(XX)                                                          \
+  XX("ftp:", 21)                                                              \
+  XX("file:", -1)                                                             \
+  XX("gopher:", 70)                                                           \
+  XX("http:", 80)                                                             \
+  XX("https:", 443)                                                           \
+  XX("ws:", 80)                                                               \
+  XX("wss:", 443)
+
+static inline bool IsSpecial(std::string scheme) {
+#define XX(name, _) if (scheme == name) return true;
+  SPECIALS(XX);
+#undef XX
+  return false;
+}
+
+// https://url.spec.whatwg.org/#start-with-a-windows-drive-letter
+static inline bool StartsWithWindowsDriveLetter(const char* p,
+                                                const char* end) {
+  const size_t length = end - p;
+  return length >= 2 &&
+    IsWindowsDriveLetter(p[0], p[1]) &&
+    (length == 2 ||
+      p[2] == '/' ||
+      p[2] == '\\' ||
+      p[2] == '?' ||
+      p[2] == '#');
+}
+
+static inline int NormalizePort(std::string scheme, int p) {
+#define XX(name, port) if (scheme == name && p == port) return -1;
+  SPECIALS(XX);
+#undef XX
+  return p;
+}
+
+#if defined(NODE_HAVE_I18N_SUPPORT)
+static inline bool ToUnicode(const std::string& input, std::string* output) {
+  MaybeStackBuffer<char> buf;
+  if (i18n::ToUnicode(&buf, input.c_str(), input.length()) < 0)
+    return false;
+  output->assign(*buf, buf.length());
+  return true;
+}
+
+static inline bool ToASCII(const std::string& input, std::string* output) {
+  MaybeStackBuffer<char> buf;
+  if (i18n::ToASCII(&buf, input.c_str(), input.length()) < 0)
+    return false;
+  output->assign(*buf, buf.length());
+  return true;
+}
+#else
+// Intentional non-ops if ICU is not present.
+static inline bool ToUnicode(const std::string& input, std::string* output) {
+  *output = input;
+  return true;
+}
+
+static inline bool ToASCII(const std::string& input, std::string* output) {
+  *output = input;
+  return true;
+}
+#endif
+
+static url_host_type ParseIPv6Host(url_host* host,
+                                   const char* input,
+                                   size_t length) {
+  url_host_type type = HOST_TYPE_FAILED;
+  for (unsigned n = 0; n < 8; n++)
+    host->value.ipv6[n] = 0;
+  uint16_t* piece_pointer = &host->value.ipv6[0];
+  uint16_t* last_piece = piece_pointer + 8;
+  uint16_t* compress_pointer = nullptr;
+  const char* pointer = input;
+  const char* end = pointer + length;
+  unsigned value, len, swaps, numbers_seen;
+  char ch = pointer < end ? pointer[0] : kEOL;
+  if (ch == ':') {
+    if (length < 2 || pointer[1] != ':')
+      goto end;
+    pointer += 2;
+    ch = pointer < end ? pointer[0] : kEOL;
+    piece_pointer++;
+    compress_pointer = piece_pointer;
+  }
+  while (ch != kEOL) {
+    if (piece_pointer > last_piece)
+      goto end;
+    if (ch == ':') {
+      if (compress_pointer != nullptr)
+        goto end;
+      pointer++;
+      ch = pointer < end ? pointer[0] : kEOL;
+      piece_pointer++;
+      compress_pointer = piece_pointer;
+      continue;
+    }
+    value = 0;
+    len = 0;
+    while (len < 4 && IsASCIIHexDigit(ch)) {
+      value = value * 0x10 + hex2bin(ch);
+      pointer++;
+      ch = pointer < end ? pointer[0] : kEOL;
+      len++;
+    }
+    switch (ch) {
+      case '.':
+        if (len == 0)
+          goto end;
+        pointer -= len;
+        ch = pointer < end ? pointer[0] : kEOL;
+        if (piece_pointer > last_piece - 2)
+          goto end;
+        numbers_seen = 0;
+        while (ch != kEOL) {
+          value = 0xffffffff;
+          if (numbers_seen > 0) {
+            if (ch == '.' && numbers_seen < 4) {
+              pointer++;
+              ch = pointer < end ? pointer[0] : kEOL;
+            } else {
+              goto end;
+            }
+          }
+          if (!IsASCIIDigit(ch))
+            goto end;
+          while (IsASCIIDigit(ch)) {
+            unsigned number = ch - '0';
+            if (value == 0xffffffff) {
+              value = number;
+            } else if (value == 0) {
+              goto end;
+            } else {
+              value = value * 10 + number;
+            }
+            if (value > 255)
+              goto end;
+            pointer++;
+            ch = pointer < end ? pointer[0] : kEOL;
+          }
+          *piece_pointer = *piece_pointer * 0x100 + value;
+          numbers_seen++;
+          if (numbers_seen == 2 || numbers_seen == 4)
+            piece_pointer++;
+        }
+        if (numbers_seen != 4)
+          goto end;
+        continue;
+      case ':':
+        pointer++;
+        ch = pointer < end ? pointer[0] : kEOL;
+        if (ch == kEOL)
+          goto end;
+        break;
+      case kEOL:
+        break;
+      default:
+        goto end;
+    }
+    *piece_pointer = value;
+    piece_pointer++;
+  }
+
+  if (compress_pointer != nullptr) {
+    swaps = piece_pointer - compress_pointer;
+    piece_pointer = last_piece - 1;
+    while (piece_pointer != &host->value.ipv6[0] && swaps > 0) {
+      uint16_t temp = *piece_pointer;
+      uint16_t* swap_piece = compress_pointer + swaps - 1;
+      *piece_pointer = *swap_piece;
+      *swap_piece = temp;
+       piece_pointer--;
+       swaps--;
+    }
+  } else if (compress_pointer == nullptr &&
+             piece_pointer != last_piece) {
+    goto end;
+  }
+  type = HOST_TYPE_IPV6;
+ end:
+  host->type = type;
+  return type;
+}
+
+static inline int64_t ParseNumber(const char* start, const char* end) {
+  unsigned R = 10;
+  if (end - start >= 2 && start[0] == '0' && (start[1] | 0x20) == 'x') {
+    start += 2;
+    R = 16;
+  }
+  if (end - start == 0) {
+    return 0;
+  } else if (R == 10 && end - start > 1 && start[0] == '0') {
+    start++;
+    R = 8;
+  }
+  const char* p = start;
+
+  while (p < end) {
+    const char ch = p[0];
+    switch (R) {
+      case 8:
+        if (ch < '0' || ch > '7')
+          return -1;
+        break;
+      case 10:
+        if (!IsASCIIDigit(ch))
+          return -1;
+        break;
+      case 16:
+        if (!IsASCIIHexDigit(ch))
+          return -1;
+        break;
+    }
+    p++;
+  }
+  return strtoll(start, NULL, R);
+}
+
+static url_host_type ParseIPv4Host(url_host* host,
+                                   const char* input,
+                                   size_t length) {
+  url_host_type type = HOST_TYPE_DOMAIN;
+  const char* pointer = input;
+  const char* mark = input;
+  const char* end = pointer + length;
+  int parts = 0;
+  uint32_t val = 0;
+  uint64_t numbers[4];
+  int tooBigNumbers = 0;
+  if (length == 0)
+    goto end;
+
+  while (pointer <= end) {
+    const char ch = pointer < end ? pointer[0] : kEOL;
+    const int remaining = end - pointer - 1;
+    if (ch == '.' || ch == kEOL) {
+      if (++parts > 4)
+        goto end;
+      if (pointer == mark)
+        goto end;
+      int64_t n = ParseNumber(mark, pointer);
+      if (n < 0)
+        goto end;
+
+      if (n > 255) {
+        tooBigNumbers++;
+      }
+      numbers[parts - 1] = n;
+      mark = pointer + 1;
+      if (ch == '.' && remaining == 0)
+        break;
+    }
+    pointer++;
+  }
+  CHECK_GT(parts, 0);
+
+  // If any but the last item in numbers is greater than 255, return failure.
+  // If the last item in numbers is greater than or equal to
+  // 256^(5 - the number of items in numbers), return failure.
+  if (tooBigNumbers > 1 ||
+      (tooBigNumbers == 1 && numbers[parts - 1] <= 255) ||
+      numbers[parts - 1] >= pow(256, static_cast<double>(5 - parts))) {
+    type = HOST_TYPE_FAILED;
+    goto end;
+  }
+
+  type = HOST_TYPE_IPV4;
+  val = numbers[parts - 1];
+  for (int n = 0; n < parts - 1; n++) {
+    double b = 3 - n;
+    val += numbers[n] * pow(256, b);
+  }
+
+  host->value.ipv4 = val;
+ end:
+  host->type = type;
+  return type;
+}
+
+static url_host_type ParseOpaqueHost(url_host* host,
+                                     const char* input,
+                                     size_t length) {
+  url_host_type type = HOST_TYPE_OPAQUE;
+  std::string output;
+  output.reserve(length * 3);
+  for (size_t i = 0; i < length; i++) {
+    const char ch = input[i];
+    if (ch != '%' && IsForbiddenHostCodePoint(ch)) {
+      type = HOST_TYPE_FAILED;
+      goto end;
+    } else {
+      AppendOrEscape(&output, ch, C0_CONTROL_ENCODE_SET);
+    }
+  }
+
+  host->value.opaque = output;
+ end:
+  host->type = type;
+  return type;
+}
+
+static url_host_type ParseHost(url_host* host,
+                               const char* input,
+                               size_t length,
+                               bool is_special,
+                               bool unicode = false) {
+  url_host_type type = HOST_TYPE_FAILED;
+  const char* pointer = input;
+  std::string decoded;
+
+  if (length == 0)
+    goto end;
+
+  if (pointer[0] == '[') {
+    if (pointer[length - 1] != ']')
+      goto end;
+    return ParseIPv6Host(host, ++pointer, length - 2);
+  }
+
+  if (!is_special)
+    return ParseOpaqueHost(host, input, length);
+
+  // First, we have to percent decode
+  PercentDecode(input, length, &decoded);
+
+  // Then we have to punycode toASCII
+  if (!ToASCII(decoded, &decoded))
+    goto end;
+
+  // If any of the following characters are still present, we have to fail
+  for (size_t n = 0; n < decoded.size(); n++) {
+    const char ch = decoded[n];
+    if (IsForbiddenHostCodePoint(ch)) {
+      goto end;
+    }
+  }
+
+  // Check to see if it's an IPv4 IP address
+  type = ParseIPv4Host(host, decoded.c_str(), decoded.length());
+  if (type == HOST_TYPE_IPV4 || type == HOST_TYPE_FAILED)
+    goto end;
+
+  // If the unicode flag is set, run the result through punycode ToUnicode
+  if (unicode && !ToUnicode(decoded, &decoded))
+    goto end;
+
+  // It's not an IPv4 or IPv6 address, it must be a domain
+  type = HOST_TYPE_DOMAIN;
+  host->value.domain = decoded;
+
+ end:
+  host->type = type;
+  return type;
+}
+
+// Locates the longest sequence of 0 segments in an IPv6 address
+// in order to use the :: compression when serializing
+static inline uint16_t* FindLongestZeroSequence(uint16_t* values,
+                                                size_t len) {
+  uint16_t* start = values;
+  uint16_t* end = start + len;
+  uint16_t* result = nullptr;
+
+  uint16_t* current = nullptr;
+  unsigned counter = 0, longest = 1;
+
+  while (start < end) {
+    if (*start == 0) {
+      if (current == nullptr)
+        current = start;
+      counter++;
+    } else {
+      if (counter > longest) {
+        longest = counter;
+        result = current;
+      }
+      counter = 0;
+      current = nullptr;
+    }
+    start++;
+  }
+  if (counter > longest)
+    result = current;
+  return result;
+}
+
+static url_host_type WriteHost(url_host* host, std::string* dest) {
+  dest->clear();
+  switch (host->type) {
+    case HOST_TYPE_DOMAIN:
+      *dest = host->value.domain;
+      break;
+    case HOST_TYPE_IPV4: {
+      dest->reserve(15);
+      uint32_t value = host->value.ipv4;
+      for (int n = 0; n < 4; n++) {
+        char buf[4];
+        char* buffer = buf;
+        snprintf(buffer, sizeof(buf), "%d", value % 256);
+        dest->insert(0, buf);
+        if (n < 3)
+          dest->insert(0, 1, '.');
+        value /= 256;
+      }
+      break;
+    }
+    case HOST_TYPE_IPV6: {
+      dest->reserve(41);
+      *dest+= '[';
+      uint16_t* start = &host->value.ipv6[0];
+      uint16_t* compress_pointer =
+          FindLongestZeroSequence(start, 8);
+      bool ignore0 = false;
+      for (int n = 0; n <= 7; n++) {
+        uint16_t* piece = &host->value.ipv6[n];
+        if (ignore0 && *piece == 0)
+          continue;
+        else if (ignore0)
+          ignore0 = false;
+        if (compress_pointer == piece) {
+          *dest += n == 0 ? "::" : ":";
+          ignore0 = true;
+          continue;
+        }
+        char buf[5];
+        char* buffer = buf;
+        snprintf(buffer, sizeof(buf), "%x", *piece);
+        *dest += buf;
+        if (n < 7)
+          *dest += ':';
+      }
+      *dest += ']';
+      break;
+    }
+    case HOST_TYPE_OPAQUE:
+      *dest = host->value.opaque;
+      break;
+    case HOST_TYPE_FAILED:
+      break;
+  }
+  return host->type;
+}
+
+static bool ParseHost(std::string* input,
+                      std::string* output,
+                      bool is_special,
+                      bool unicode = false) {
+  if (input->length() == 0) {
+    output->clear();
+    return true;
+  }
+  url_host host{{""}, HOST_TYPE_DOMAIN};
+  ParseHost(&host, input->c_str(), input->length(), is_special, unicode);
+  if (host.type == HOST_TYPE_FAILED)
+    return false;
+  WriteHost(&host, output);
+  return true;
+}
+
+static inline void Copy(Environment* env,
+                        Local<Array> ary,
+                        std::vector<std::string>* vec) {
+  const int32_t len = ary->Length();
+  if (len == 0)
+    return;  // nothing to copy
+  vec->reserve(len);
+  for (int32_t n = 0; n < len; n++) {
+    Local<Value> val = ary->Get(env->context(), n).ToLocalChecked();
+    if (val->IsString()) {
+      Utf8Value value(env->isolate(), val.As<String>());
+      vec->push_back(std::string(*value, value.length()));
+    }
+  }
+}
+
+static inline Local<Array> Copy(Environment* env,
+                                const std::vector<std::string>& vec) {
+  Isolate* isolate = env->isolate();
+  Local<Array> ary = Array::New(isolate, vec.size());
+  for (size_t n = 0; n < vec.size(); n++)
+    ary->Set(env->context(), n, UTF8STRING(isolate, vec[n])).FromJust();
+  return ary;
+}
+
+static inline void HarvestBase(Environment* env,
+                               struct url_data* base,
+                               Local<Object> base_obj) {
+  Local<Context> context = env->context();
+  Local<Value> flags = GET(env, base_obj, "flags");
+  if (flags->IsInt32())
+    base->flags = flags->Int32Value(context).FromJust();
+
+  Local<Value> scheme = GET(env, base_obj, "scheme");
+  base->scheme = Utf8Value(env->isolate(), scheme).out();
+
+  GET_AND_SET(env, base_obj, username, base, URL_FLAGS_HAS_USERNAME);
+  GET_AND_SET(env, base_obj, password, base, URL_FLAGS_HAS_PASSWORD);
+  GET_AND_SET(env, base_obj, host, base, URL_FLAGS_HAS_HOST);
+  GET_AND_SET(env, base_obj, query, base, URL_FLAGS_HAS_QUERY);
+  GET_AND_SET(env, base_obj, fragment, base, URL_FLAGS_HAS_FRAGMENT);
+  Local<Value> port = GET(env, base_obj, "port");
+  if (port->IsInt32())
+    base->port = port->Int32Value(context).FromJust();
+  Local<Value> path = GET(env, base_obj, "path");
+  if (path->IsArray()) {
+    base->flags |= URL_FLAGS_HAS_PATH;
+    Copy(env, path.As<Array>(), &(base->path));
+  }
+}
+
+static inline void HarvestContext(Environment* env,
+                                  struct url_data* context,
+                                  Local<Object> context_obj) {
+  Local<Value> flags = GET(env, context_obj, "flags");
+  if (flags->IsInt32()) {
+    int32_t _flags = flags->Int32Value(env->context()).FromJust();
+    if (_flags & URL_FLAGS_SPECIAL)
+      context->flags |= URL_FLAGS_SPECIAL;
+    if (_flags & URL_FLAGS_CANNOT_BE_BASE)
+      context->flags |= URL_FLAGS_CANNOT_BE_BASE;
+    if (_flags & URL_FLAGS_HAS_USERNAME)
+      context->flags |= URL_FLAGS_HAS_USERNAME;
+    if (_flags & URL_FLAGS_HAS_PASSWORD)
+      context->flags |= URL_FLAGS_HAS_PASSWORD;
+    if (_flags & URL_FLAGS_HAS_HOST)
+      context->flags |= URL_FLAGS_HAS_HOST;
+  }
+  Local<Value> scheme = GET(env, context_obj, "scheme");
+  if (scheme->IsString()) {
+    Utf8Value value(env->isolate(), scheme);
+    context->scheme.assign(*value, value.length());
+  }
+  Local<Value> port = GET(env, context_obj, "port");
+  if (port->IsInt32())
+    context->port = port->Int32Value(env->context()).FromJust();
+  if (context->flags & URL_FLAGS_HAS_USERNAME) {
+    Local<Value> username = GET(env, context_obj, "username");
+    CHECK(username->IsString());
+    Utf8Value value(env->isolate(), username);
+    context->username.assign(*value, value.length());
+  }
+  if (context->flags & URL_FLAGS_HAS_PASSWORD) {
+    Local<Value> password = GET(env, context_obj, "password");
+    CHECK(password->IsString());
+    Utf8Value value(env->isolate(), password);
+    context->password.assign(*value, value.length());
+  }
+  Local<Value> host = GET(env, context_obj, "host");
+  if (host->IsString()) {
+    Utf8Value value(env->isolate(), host);
+    context->host.assign(*value, value.length());
+  }
+}
+
+// Single dot segment can be ".", "%2e", or "%2E"
+static inline bool IsSingleDotSegment(std::string str) {
+  switch (str.size()) {
+    case 1:
+      return str == ".";
+    case 3:
+      return str[0] == '%' &&
+             str[1] == '2' &&
+             ASCIILowercase(str[2]) == 'e';
+    default:
+      return false;
+  }
+}
+
+// Double dot segment can be:
+//   "..", ".%2e", ".%2E", "%2e.", "%2E.",
+//   "%2e%2e", "%2E%2E", "%2e%2E", or "%2E%2e"
+static inline bool IsDoubleDotSegment(std::string str) {
+  switch (str.size()) {
+    case 2:
+      return str == "..";
+    case 4:
+      if (str[0] != '.' && str[0] != '%')
+        return false;
+      return ((str[0] == '.' &&
+               str[1] == '%' &&
+               str[2] == '2' &&
+               ASCIILowercase(str[3]) == 'e') ||
+              (str[0] == '%' &&
+               str[1] == '2' &&
+               ASCIILowercase(str[2]) == 'e' &&
+               str[3] == '.'));
+    case 6:
+      return (str[0] == '%' &&
+              str[1] == '2' &&
+              ASCIILowercase(str[2]) == 'e' &&
+              str[3] == '%' &&
+              str[4] == '2' &&
+              ASCIILowercase(str[5]) == 'e');
+    default:
+      return false;
+  }
+}
+
+static inline void ShortenUrlPath(struct url_data* url) {
+  if (url->path.empty()) return;
+  if (url->path.size() == 1 && url->scheme == "file:" &&
+      IsNormalizedWindowsDriveLetter(url->path[0])) return;
+  url->path.pop_back();
+}
+
+void URL::Parse(const char* input,
+                size_t len,
+                enum url_parse_state state_override,
+                struct url_data* url,
+                bool has_url,
+                const struct url_data* base,
+                bool has_base) {
+  const char* p = input;
+  const char* end = input + len;
+
+  if (!has_url) {
+    for (const char* ptr = p; ptr < end; ptr++) {
+      if (IsC0ControlOrSpace(*ptr))
+        p++;
+      else
+        break;
+    }
+    for (const char* ptr = end - 1; ptr >= p; ptr--) {
+      if (IsC0ControlOrSpace(*ptr))
+        end--;
+      else
+        break;
+    }
+    len = end - p;
+  }
+
+  std::string whitespace_stripped;
+  whitespace_stripped.reserve(len);
+  for (const char* ptr = p; ptr < end; ptr++)
+    if (!IsASCIITabOrNewline(*ptr))
+      whitespace_stripped += *ptr;
+
+  input = whitespace_stripped.c_str();
+  len = whitespace_stripped.size();
+  p = input;
+  end = input + len;
+
+  bool atflag = false;
+  bool sbflag = false;
+  bool uflag = false;
+
+  std::string buffer;
+  url->scheme.reserve(len);
+  url->username.reserve(len);
+  url->password.reserve(len);
+  url->host.reserve(len);
+  url->path.reserve(len);
+  url->query.reserve(len);
+  url->fragment.reserve(len);
+  buffer.reserve(len);
+
+  // Set the initial parse state.
+  const bool has_state_override = state_override != kUnknownState;
+  enum url_parse_state state = has_state_override ? state_override :
+                                                    kSchemeStart;
+
+  if (state < kSchemeStart || state > kFragment) {
+    url->flags |= URL_FLAGS_INVALID_PARSE_STATE;
+    return;
+  }
+
+  while (p <= end) {
+    const char ch = p < end ? p[0] : kEOL;
+    bool special = (url->flags & URL_FLAGS_SPECIAL);
+    bool cannot_be_base;
+    const bool special_back_slash = (special && ch == '\\');
+
+    switch (state) {
+      case kSchemeStart:
+        if (IsASCIIAlpha(ch)) {
+          buffer += ASCIILowercase(ch);
+          state = kScheme;
+        } else if (!has_state_override) {
+          state = kNoScheme;
+          continue;
+        } else {
+          url->flags |= URL_FLAGS_FAILED;
+          return;
+        }
+        break;
+      case kScheme:
+        if (IsASCIIAlphanumeric(ch) || ch == '+' || ch == '-' || ch == '.') {
+          buffer += ASCIILowercase(ch);
+        } else if (ch == ':' || (has_state_override && ch == kEOL)) {
+          if (has_state_override && buffer.size() == 0) {
+            url->flags |= URL_FLAGS_TERMINATED;
+            return;
+          }
+          buffer += ':';
+
+          bool new_is_special = IsSpecial(buffer);
+
+          if (has_state_override) {
+            if ((special != new_is_special) ||
+                ((buffer == "file:") &&
+                 ((url->flags & URL_FLAGS_HAS_USERNAME) ||
+                  (url->flags & URL_FLAGS_HAS_PASSWORD) ||
+                  (url->port != -1)))) {
+              url->flags |= URL_FLAGS_TERMINATED;
+              return;
+            }
+
+            // File scheme && (host == empty or null) check left to JS-land
+            // as it can be done before even entering C++ binding.
+          }
+
+          url->scheme = buffer;
+          url->port = NormalizePort(url->scheme, url->port);
+          if (new_is_special) {
+            url->flags |= URL_FLAGS_SPECIAL;
+            special = true;
+          } else {
+            url->flags &= ~URL_FLAGS_SPECIAL;
+            special = false;
+          }
+          buffer.clear();
+          if (has_state_override)
+            return;
+          if (url->scheme == "file:") {
+            state = kFile;
+          } else if (special &&
+                     has_base &&
+                     url->scheme == base->scheme) {
+            state = kSpecialRelativeOrAuthority;
+          } else if (special) {
+            state = kSpecialAuthoritySlashes;
+          } else if (p[1] == '/') {
+            state = kPathOrAuthority;
+            p++;
+          } else {
+            url->flags |= URL_FLAGS_CANNOT_BE_BASE;
+            url->flags |= URL_FLAGS_HAS_PATH;
+            url->path.push_back("");
+            state = kCannotBeBase;
+          }
+        } else if (!has_state_override) {
+          buffer.clear();
+          state = kNoScheme;
+          p = input;
+          continue;
+        } else {
+          url->flags |= URL_FLAGS_FAILED;
+          return;
+        }
+        break;
+      case kNoScheme:
+        cannot_be_base = has_base && (base->flags & URL_FLAGS_CANNOT_BE_BASE);
+        if (!has_base || (cannot_be_base && ch != '#')) {
+          url->flags |= URL_FLAGS_FAILED;
+          return;
+        } else if (cannot_be_base && ch == '#') {
+          url->scheme = base->scheme;
+          if (IsSpecial(url->scheme)) {
+            url->flags |= URL_FLAGS_SPECIAL;
+            special = true;
+          } else {
+            url->flags &= ~URL_FLAGS_SPECIAL;
+            special = false;
+          }
+          if (base->flags & URL_FLAGS_HAS_PATH) {
+            url->flags |= URL_FLAGS_HAS_PATH;
+            url->path = base->path;
+          }
+          if (base->flags & URL_FLAGS_HAS_QUERY) {
+            url->flags |= URL_FLAGS_HAS_QUERY;
+            url->query = base->query;
+          }
+          if (base->flags & URL_FLAGS_HAS_FRAGMENT) {
+            url->flags |= URL_FLAGS_HAS_FRAGMENT;
+            url->fragment = base->fragment;
+          }
+          url->flags |= URL_FLAGS_CANNOT_BE_BASE;
+          state = kFragment;
+        } else if (has_base &&
+                   base->scheme != "file:") {
+          state = kRelative;
+          continue;
+        } else {
+          url->scheme = "file:";
+          url->flags |= URL_FLAGS_SPECIAL;
+          special = true;
+          state = kFile;
+          continue;
+        }
+        break;
+      case kSpecialRelativeOrAuthority:
+        if (ch == '/' && p[1] == '/') {
+          state = kSpecialAuthorityIgnoreSlashes;
+          p++;
+        } else {
+          state = kRelative;
+          continue;
+        }
+        break;
+      case kPathOrAuthority:
+        if (ch == '/') {
+          state = kAuthority;
+        } else {
+          state = kPath;
+          continue;
+        }
+        break;
+      case kRelative:
+        url->scheme = base->scheme;
+        if (IsSpecial(url->scheme)) {
+          url->flags |= URL_FLAGS_SPECIAL;
+          special = true;
+        } else {
+          url->flags &= ~URL_FLAGS_SPECIAL;
+          special = false;
+        }
+        switch (ch) {
+          case kEOL:
+            if (base->flags & URL_FLAGS_HAS_USERNAME) {
+              url->flags |= URL_FLAGS_HAS_USERNAME;
+              url->username = base->username;
+            }
+            if (base->flags & URL_FLAGS_HAS_PASSWORD) {
+              url->flags |= URL_FLAGS_HAS_PASSWORD;
+              url->password = base->password;
+            }
+            if (base->flags & URL_FLAGS_HAS_HOST) {
+              url->flags |= URL_FLAGS_HAS_HOST;
+              url->host = base->host;
+            }
+            if (base->flags & URL_FLAGS_HAS_QUERY) {
+              url->flags |= URL_FLAGS_HAS_QUERY;
+              url->query = base->query;
+            }
+            if (base->flags & URL_FLAGS_HAS_PATH) {
+              url->flags |= URL_FLAGS_HAS_PATH;
+              url->path = base->path;
+            }
+            url->port = base->port;
+            break;
+          case '/':
+            state = kRelativeSlash;
+            break;
+          case '?':
+            if (base->flags & URL_FLAGS_HAS_USERNAME) {
+              url->flags |= URL_FLAGS_HAS_USERNAME;
+              url->username = base->username;
+            }
+            if (base->flags & URL_FLAGS_HAS_PASSWORD) {
+              url->flags |= URL_FLAGS_HAS_PASSWORD;
+              url->password = base->password;
+            }
+            if (base->flags & URL_FLAGS_HAS_HOST) {
+              url->flags |= URL_FLAGS_HAS_HOST;
+              url->host = base->host;
+            }
+            if (base->flags & URL_FLAGS_HAS_PATH) {
+              url->flags |= URL_FLAGS_HAS_PATH;
+              url->path = base->path;
+            }
+            url->port = base->port;
+            state = kQuery;
+            break;
+          case '#':
+            if (base->flags & URL_FLAGS_HAS_USERNAME) {
+              url->flags |= URL_FLAGS_HAS_USERNAME;
+              url->username = base->username;
+            }
+            if (base->flags & URL_FLAGS_HAS_PASSWORD) {
+              url->flags |= URL_FLAGS_HAS_PASSWORD;
+              url->password = base->password;
+            }
+            if (base->flags & URL_FLAGS_HAS_HOST) {
+              url->flags |= URL_FLAGS_HAS_HOST;
+              url->host = base->host;
+            }
+            if (base->flags & URL_FLAGS_HAS_QUERY) {
+              url->flags |= URL_FLAGS_HAS_QUERY;
+              url->query = base->query;
+            }
+            if (base->flags & URL_FLAGS_HAS_PATH) {
+              url->flags |= URL_FLAGS_HAS_PATH;
+              url->path = base->path;
+            }
+            url->port = base->port;
+            state = kFragment;
+            break;
+          default:
+            if (special_back_slash) {
+              state = kRelativeSlash;
+            } else {
+              if (base->flags & URL_FLAGS_HAS_USERNAME) {
+                url->flags |= URL_FLAGS_HAS_USERNAME;
+                url->username = base->username;
+              }
+              if (base->flags & URL_FLAGS_HAS_PASSWORD) {
+                url->flags |= URL_FLAGS_HAS_PASSWORD;
+                url->password = base->password;
+              }
+              if (base->flags & URL_FLAGS_HAS_HOST) {
+                url->flags |= URL_FLAGS_HAS_HOST;
+                url->host = base->host;
+              }
+              if (base->flags & URL_FLAGS_HAS_PATH) {
+                url->flags |= URL_FLAGS_HAS_PATH;
+                url->path = base->path;
+                ShortenUrlPath(url);
+              }
+              url->port = base->port;
+              state = kPath;
+              continue;
+            }
+        }
+        break;
+      case kRelativeSlash:
+        if (IsSpecial(url->scheme) && (ch == '/' || ch == '\\')) {
+          state = kSpecialAuthorityIgnoreSlashes;
+        } else if (ch == '/') {
+          state = kAuthority;
+        } else {
+          if (base->flags & URL_FLAGS_HAS_USERNAME) {
+            url->flags |= URL_FLAGS_HAS_USERNAME;
+            url->username = base->username;
+          }
+          if (base->flags & URL_FLAGS_HAS_PASSWORD) {
+            url->flags |= URL_FLAGS_HAS_PASSWORD;
+            url->password = base->password;
+          }
+          if (base->flags & URL_FLAGS_HAS_HOST) {
+            url->flags |= URL_FLAGS_HAS_HOST;
+            url->host = base->host;
+          }
+          url->port = base->port;
+          state = kPath;
+          continue;
+        }
+        break;
+      case kSpecialAuthoritySlashes:
+        state = kSpecialAuthorityIgnoreSlashes;
+        if (ch == '/' && p[1] == '/') {
+          p++;
+        } else {
+          continue;
+        }
+        break;
+      case kSpecialAuthorityIgnoreSlashes:
+        if (ch != '/' && ch != '\\') {
+          state = kAuthority;
+          continue;
+        }
+        break;
+      case kAuthority:
+        if (ch == '@') {
+          if (atflag) {
+            buffer.reserve(buffer.size() + 3);
+            buffer.insert(0, "%40");
+          }
+          atflag = true;
+          const size_t blen = buffer.size();
+          if (blen > 0 && buffer[0] != ':') {
+            url->flags |= URL_FLAGS_HAS_USERNAME;
+          }
+          for (size_t n = 0; n < blen; n++) {
+            const char bch = buffer[n];
+            if (bch == ':') {
+              url->flags |= URL_FLAGS_HAS_PASSWORD;
+              if (!uflag) {
+                uflag = true;
+                continue;
+              }
+            }
+            if (uflag) {
+              AppendOrEscape(&url->password, bch, USERINFO_ENCODE_SET);
+            } else {
+              AppendOrEscape(&url->username, bch, USERINFO_ENCODE_SET);
+            }
+          }
+          buffer.clear();
+        } else if (ch == kEOL ||
+                   ch == '/' ||
+                   ch == '?' ||
+                   ch == '#' ||
+                   special_back_slash) {
+          if (atflag && buffer.size() == 0) {
+            url->flags |= URL_FLAGS_FAILED;
+            return;
+          }
+          p -= buffer.size() + 1;
+          buffer.clear();
+          state = kHost;
+        } else {
+          buffer += ch;
+        }
+        break;
+      case kHost:
+      case kHostname:
+        if (has_state_override && url->scheme == "file:") {
+          state = kFileHost;
+          continue;
+        } else if (ch == ':' && !sbflag) {
+          if (buffer.size() == 0) {
+            url->flags |= URL_FLAGS_FAILED;
+            return;
+          }
+          url->flags |= URL_FLAGS_HAS_HOST;
+          if (!ParseHost(&buffer, &url->host, special)) {
+            url->flags |= URL_FLAGS_FAILED;
+            return;
+          }
+          buffer.clear();
+          state = kPort;
+          if (state_override == kHostname) {
+            return;
+          }
+        } else if (ch == kEOL ||
+                   ch == '/' ||
+                   ch == '?' ||
+                   ch == '#' ||
+                   special_back_slash) {
+          p--;
+          if (special && buffer.size() == 0) {
+            url->flags |= URL_FLAGS_FAILED;
+            return;
+          }
+          if (has_state_override &&
+              buffer.size() == 0 &&
+              ((url->username.size() > 0 || url->password.size() > 0) ||
+               url->port != -1)) {
+            url->flags |= URL_FLAGS_TERMINATED;
+            return;
+          }
+          url->flags |= URL_FLAGS_HAS_HOST;
+          if (!ParseHost(&buffer, &url->host, special)) {
+            url->flags |= URL_FLAGS_FAILED;
+            return;
+          }
+          buffer.clear();
+          state = kPathStart;
+          if (has_state_override) {
+            return;
+          }
+        } else {
+          if (ch == '[')
+            sbflag = true;
+          if (ch == ']')
+            sbflag = false;
+          buffer += ch;
+        }
+        break;
+      case kPort:
+        if (IsASCIIDigit(ch)) {
+          buffer += ch;
+        } else if (has_state_override ||
+                   ch == kEOL ||
+                   ch == '/' ||
+                   ch == '?' ||
+                   ch == '#' ||
+                   special_back_slash) {
+          if (buffer.size() > 0) {
+            unsigned port = 0;
+            // the condition port <= 0xffff prevents integer overflow
+            for (size_t i = 0; port <= 0xffff && i < buffer.size(); i++)
+              port = port * 10 + buffer[i] - '0';
+            if (port > 0xffff) {
+              // TODO(TimothyGu): This hack is currently needed for the host
+              // setter since it needs access to hostname if it is valid, and
+              // if the FAILED flag is set the entire response to JS layer
+              // will be empty.
+              if (state_override == kHost)
+                url->port = -1;
+              else
+                url->flags |= URL_FLAGS_FAILED;
+              return;
+            }
+            // the port is valid
+            url->port = NormalizePort(url->scheme, static_cast<int>(port));
+            buffer.clear();
+          } else if (has_state_override) {
+            // TODO(TimothyGu): Similar case as above.
+            if (state_override == kHost)
+              url->port = -1;
+            else
+              url->flags |= URL_FLAGS_TERMINATED;
+            return;
+          }
+          state = kPathStart;
+          continue;
+        } else {
+          url->flags |= URL_FLAGS_FAILED;
+          return;
+        }
+        break;
+      case kFile:
+        url->scheme = "file:";
+        if (ch == '/' || ch == '\\') {
+          state = kFileSlash;
+        } else if (has_base && base->scheme == "file:") {
+          switch (ch) {
+            case kEOL:
+              if (base->flags & URL_FLAGS_HAS_HOST) {
+                url->flags |= URL_FLAGS_HAS_HOST;
+                url->host = base->host;
+              }
+              if (base->flags & URL_FLAGS_HAS_PATH) {
+                url->flags |= URL_FLAGS_HAS_PATH;
+                url->path = base->path;
+              }
+              if (base->flags & URL_FLAGS_HAS_QUERY) {
+                url->flags |= URL_FLAGS_HAS_QUERY;
+                url->query = base->query;
+              }
+              break;
+            case '?':
+              if (base->flags & URL_FLAGS_HAS_HOST) {
+                url->flags |= URL_FLAGS_HAS_HOST;
+                url->host = base->host;
+              }
+              if (base->flags & URL_FLAGS_HAS_PATH) {
+                url->flags |= URL_FLAGS_HAS_PATH;
+                url->path = base->path;
+              }
+              url->flags |= URL_FLAGS_HAS_QUERY;
+              url->query.clear();
+              state = kQuery;
+              break;
+            case '#':
+              if (base->flags & URL_FLAGS_HAS_HOST) {
+                url->flags |= URL_FLAGS_HAS_HOST;
+                url->host = base->host;
+              }
+              if (base->flags & URL_FLAGS_HAS_PATH) {
+                url->flags |= URL_FLAGS_HAS_PATH;
+                url->path = base->path;
+              }
+              if (base->flags & URL_FLAGS_HAS_QUERY) {
+                url->flags |= URL_FLAGS_HAS_QUERY;
+                url->query = base->query;
+              }
+              url->flags |= URL_FLAGS_HAS_FRAGMENT;
+              url->fragment.clear();
+              state = kFragment;
+              break;
+            default:
+              if (!StartsWithWindowsDriveLetter(p, end)) {
+                if (base->flags & URL_FLAGS_HAS_HOST) {
+                  url->flags |= URL_FLAGS_HAS_HOST;
+                  url->host = base->host;
+                }
+                if (base->flags & URL_FLAGS_HAS_PATH) {
+                  url->flags |= URL_FLAGS_HAS_PATH;
+                  url->path = base->path;
+                }
+                ShortenUrlPath(url);
+              }
+              state = kPath;
+              continue;
+          }
+        } else {
+          state = kPath;
+          continue;
+        }
+        break;
+      case kFileSlash:
+        if (ch == '/' || ch == '\\') {
+          state = kFileHost;
+        } else {
+          if (has_base &&
+              base->scheme == "file:" &&
+              !StartsWithWindowsDriveLetter(p, end)) {
+            if (IsNormalizedWindowsDriveLetter(base->path[0])) {
+              url->flags |= URL_FLAGS_HAS_PATH;
+              url->path.push_back(base->path[0]);
+            } else {
+              if (base->flags & URL_FLAGS_HAS_HOST) {
+                url->flags |= URL_FLAGS_HAS_HOST;
+                url->host = base->host;
+              } else {
+                url->flags &= ~URL_FLAGS_HAS_HOST;
+                url->host.clear();
+              }
+            }
+          }
+          state = kPath;
+          continue;
+        }
+        break;
+      case kFileHost:
+        if (ch == kEOL ||
+            ch == '/' ||
+            ch == '\\' ||
+            ch == '?' ||
+            ch == '#') {
+          if (!has_state_override &&
+              buffer.size() == 2 &&
+              IsWindowsDriveLetter(buffer)) {
+            state = kPath;
+          } else if (buffer.size() == 0) {
+            url->flags |= URL_FLAGS_HAS_HOST;
+            url->host.clear();
+            if (has_state_override)
+              return;
+            state = kPathStart;
+          } else {
+            std::string host;
+            if (!ParseHost(&buffer, &host, special)) {
+              url->flags |= URL_FLAGS_FAILED;
+              return;
+            }
+            if (host == "localhost")
+              host.clear();
+            url->flags |= URL_FLAGS_HAS_HOST;
+            url->host = host;
+            if (has_state_override)
+              return;
+            buffer.clear();
+            state = kPathStart;
+          }
+          continue;
+        } else {
+          buffer += ch;
+        }
+        break;
+      case kPathStart:
+        if (IsSpecial(url->scheme)) {
+          state = kPath;
+          if (ch != '/' && ch != '\\') {
+            continue;
+          }
+        } else if (!has_state_override && ch == '?') {
+          url->flags |= URL_FLAGS_HAS_QUERY;
+          url->query.clear();
+          state = kQuery;
+        } else if (!has_state_override && ch == '#') {
+          url->flags |= URL_FLAGS_HAS_FRAGMENT;
+          url->fragment.clear();
+          state = kFragment;
+        } else if (ch != kEOL) {
+          state = kPath;
+          if (ch != '/') {
+            continue;
+          }
+        }
+        break;
+      case kPath:
+        if (ch == kEOL ||
+            ch == '/' ||
+            special_back_slash ||
+            (!has_state_override && (ch == '?' || ch == '#'))) {
+          if (IsDoubleDotSegment(buffer)) {
+            ShortenUrlPath(url);
+            if (ch != '/' && !special_back_slash) {
+              url->flags |= URL_FLAGS_HAS_PATH;
+              url->path.push_back("");
+            }
+          } else if (IsSingleDotSegment(buffer) &&
+                     ch != '/' && !special_back_slash) {
+            url->flags |= URL_FLAGS_HAS_PATH;
+            url->path.push_back("");
+          } else if (!IsSingleDotSegment(buffer)) {
+            if (url->scheme == "file:" &&
+                url->path.empty() &&
+                buffer.size() == 2 &&
+                IsWindowsDriveLetter(buffer)) {
+              if ((url->flags & URL_FLAGS_HAS_HOST) &&
+                  !url->host.empty()) {
+                url->host.clear();
+                url->flags |= URL_FLAGS_HAS_HOST;
+              }
+              buffer[1] = ':';
+            }
+            url->flags |= URL_FLAGS_HAS_PATH;
+            std::string segment(buffer.c_str(), buffer.size());
+            url->path.push_back(segment);
+          }
+          buffer.clear();
+          if (url->scheme == "file:" &&
+              (ch == kEOL ||
+               ch == '?' ||
+               ch == '#')) {
+            while (url->path.size() > 1 && url->path[0].length() == 0) {
+              url->path.erase(url->path.begin());
+            }
+          }
+          if (ch == '?') {
+            url->flags |= URL_FLAGS_HAS_QUERY;
+            state = kQuery;
+          } else if (ch == '#') {
+            state = kFragment;
+          }
+        } else {
+          AppendOrEscape(&buffer, ch, PATH_ENCODE_SET);
+        }
+        break;
+      case kCannotBeBase:
+        switch (ch) {
+          case '?':
+            state = kQuery;
+            break;
+          case '#':
+            state = kFragment;
+            break;
+          default:
+            if (url->path.size() == 0)
+              url->path.push_back("");
+            if (url->path.size() > 0 && ch != kEOL)
+              AppendOrEscape(&url->path[0], ch, C0_CONTROL_ENCODE_SET);
+        }
+        break;
+      case kQuery:
+        if (ch == kEOL || (!has_state_override && ch == '#')) {
+          url->flags |= URL_FLAGS_HAS_QUERY;
+          url->query = buffer;
+          buffer.clear();
+          if (ch == '#')
+            state = kFragment;
+        } else {
+          AppendOrEscape(&buffer, ch, QUERY_ENCODE_SET);
+        }
+        break;
+      case kFragment:
+        switch (ch) {
+          case kEOL:
+            url->flags |= URL_FLAGS_HAS_FRAGMENT;
+            url->fragment = buffer;
+            break;
+          case 0:
+            break;
+          default:
+            AppendOrEscape(&buffer, ch, C0_CONTROL_ENCODE_SET);
+        }
+        break;
+      default:
+        url->flags |= URL_FLAGS_INVALID_PARSE_STATE;
+        return;
+    }
+
+    p++;
+  }
+}  // NOLINT(readability/fn_size)
+
+static inline void SetArgs(Environment* env,
+                           Local<Value> argv[],
+                           const struct url_data* url) {
+  Isolate* isolate = env->isolate();
+  argv[ARG_FLAGS] = Integer::NewFromUnsigned(isolate, url->flags);
+  argv[ARG_PROTOCOL] = OneByteString(isolate, url->scheme.c_str());
+  if (url->flags & URL_FLAGS_HAS_USERNAME)
+    argv[ARG_USERNAME] = UTF8STRING(isolate, url->username);
+  if (url->flags & URL_FLAGS_HAS_PASSWORD)
+    argv[ARG_PASSWORD] = UTF8STRING(isolate, url->password);
+  if (url->flags & URL_FLAGS_HAS_HOST)
+    argv[ARG_HOST] = UTF8STRING(isolate, url->host);
+  if (url->flags & URL_FLAGS_HAS_QUERY)
+    argv[ARG_QUERY] = UTF8STRING(isolate, url->query);
+  if (url->flags & URL_FLAGS_HAS_FRAGMENT)
+    argv[ARG_FRAGMENT] = UTF8STRING(isolate, url->fragment);
+  if (url->port > -1)
+    argv[ARG_PORT] = Integer::New(isolate, url->port);
+  if (url->flags & URL_FLAGS_HAS_PATH)
+    argv[ARG_PATH] = Copy(env, url->path);
+}
+
+static void Parse(Environment* env,
+                  Local<Value> recv,
+                  const char* input,
+                  const size_t len,
+                  enum url_parse_state state_override,
+                  Local<Value> base_obj,
+                  Local<Value> context_obj,
+                  Local<Function> cb,
+                  Local<Value> error_cb) {
+  Isolate* isolate = env->isolate();
+  Local<Context> context = env->context();
+  HandleScope handle_scope(isolate);
+  Context::Scope context_scope(context);
+
+  const bool has_context = context_obj->IsObject();
+  const bool has_base = base_obj->IsObject();
+
+  struct url_data base;
+  struct url_data url;
+  if (has_context)
+    HarvestContext(env, &url, context_obj.As<Object>());
+  if (has_base)
+    HarvestBase(env, &base, base_obj.As<Object>());
+
+  URL::Parse(input, len, state_override, &url, has_context, &base, has_base);
+  if ((url.flags & URL_FLAGS_INVALID_PARSE_STATE) ||
+      ((state_override != kUnknownState) &&
+       (url.flags & URL_FLAGS_TERMINATED)))
+    return;
+
+  // Define the return value placeholders
+  const Local<Value> undef = Undefined(isolate);
+  const Local<Value> null = Null(isolate);
+  if (!(url.flags & URL_FLAGS_FAILED)) {
+    Local<Value> argv[9] = {
+      undef,
+      undef,
+      undef,
+      undef,
+      null,  // host defaults to null
+      null,  // port defaults to null
+      undef,
+      null,  // query defaults to null
+      null,  // fragment defaults to null
+    };
+    SetArgs(env, argv, &url);
+    cb->Call(context, recv, arraysize(argv), argv).FromMaybe(Local<Value>());
+  } else if (error_cb->IsFunction()) {
+    Local<Value> argv[2] = { undef, undef };
+    argv[ERR_ARG_FLAGS] = Integer::NewFromUnsigned(isolate, url.flags);
+    argv[ERR_ARG_INPUT] =
+      String::NewFromUtf8(env->isolate(),
+                          input,
+                          v8::NewStringType::kNormal).ToLocalChecked();
+    error_cb.As<Function>()->Call(context, recv, arraysize(argv), argv)
+        .FromMaybe(Local<Value>());
+  }
+}
+
+static void Parse(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  CHECK_GE(args.Length(), 5);
+  CHECK(args[0]->IsString());  // input
+  CHECK(args[2]->IsUndefined() ||  // base context
+        args[2]->IsNull() ||
+        args[2]->IsObject());
+  CHECK(args[3]->IsUndefined() ||  // context
+        args[3]->IsNull() ||
+        args[3]->IsObject());
+  CHECK(args[4]->IsFunction());  // complete callback
+  CHECK(args[5]->IsUndefined() || args[5]->IsFunction());  // error callback
+
+  Utf8Value input(env->isolate(), args[0]);
+  enum url_parse_state state_override = kUnknownState;
+  if (args[1]->IsNumber()) {
+    state_override = static_cast<enum url_parse_state>(
+        args[1]->Uint32Value(env->context()).FromJust());
+  }
+
+  Parse(env, args.This(),
+        *input, input.length(),
+        state_override,
+        args[2],
+        args[3],
+        args[4].As<Function>(),
+        args[5]);
+}
+
+static void EncodeAuthSet(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  CHECK_GE(args.Length(), 1);
+  CHECK(args[0]->IsString());
+  Utf8Value value(env->isolate(), args[0]);
+  std::string output;
+  const size_t len = value.length();
+  output.reserve(len);
+  for (size_t n = 0; n < len; n++) {
+    const char ch = (*value)[n];
+    AppendOrEscape(&output, ch, USERINFO_ENCODE_SET);
+  }
+  args.GetReturnValue().Set(
+      String::NewFromUtf8(env->isolate(),
+                          output.c_str(),
+                          v8::NewStringType::kNormal).ToLocalChecked());
+}
+
+static void ToUSVString(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  CHECK_GE(args.Length(), 2);
+  CHECK(args[0]->IsString());
+  CHECK(args[1]->IsNumber());
+
+  TwoByteValue value(env->isolate(), args[0]);
+  const size_t n = value.length();
+
+  const int64_t start = args[1]->IntegerValue(env->context()).FromJust();
+  CHECK_GE(start, 0);
+
+  for (size_t i = start; i < n; i++) {
+    char16_t c = value[i];
+    if (!IsUnicodeSurrogate(c)) {
+      continue;
+    } else if (IsUnicodeSurrogateTrail(c) || i == n - 1) {
+      value[i] = kUnicodeReplacementCharacter;
+    } else {
+      char16_t d = value[i + 1];
+      if (IsUnicodeTrail(d)) {
+        i++;
+      } else {
+        value[i] = kUnicodeReplacementCharacter;
+      }
+    }
+  }
+
+  args.GetReturnValue().Set(
+      String::NewFromTwoByte(env->isolate(),
+                             *value,
+                             v8::NewStringType::kNormal,
+                             n).ToLocalChecked());
+}
+
+static void DomainToASCII(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  CHECK_GE(args.Length(), 1);
+  CHECK(args[0]->IsString());
+  Utf8Value value(env->isolate(), args[0]);
+
+  url_host host{{""}, HOST_TYPE_DOMAIN};
+  // Assuming the host is used for a special scheme.
+  ParseHost(&host, *value, value.length(), true);
+  if (host.type == HOST_TYPE_FAILED) {
+    args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), ""));
+    return;
+  }
+  std::string out;
+  WriteHost(&host, &out);
+  args.GetReturnValue().Set(
+      String::NewFromUtf8(env->isolate(),
+                          out.c_str(),
+                          v8::NewStringType::kNormal).ToLocalChecked());
+}
+
+static void DomainToUnicode(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  CHECK_GE(args.Length(), 1);
+  CHECK(args[0]->IsString());
+  Utf8Value value(env->isolate(), args[0]);
+
+  url_host host{{""}, HOST_TYPE_DOMAIN};
+  // Assuming the host is used for a special scheme.
+  ParseHost(&host, *value, value.length(), true, true);
+  if (host.type == HOST_TYPE_FAILED) {
+    args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), ""));
+    return;
+  }
+  std::string out;
+  WriteHost(&host, &out);
+  args.GetReturnValue().Set(
+      String::NewFromUtf8(env->isolate(),
+                          out.c_str(),
+                          v8::NewStringType::kNormal).ToLocalChecked());
+}
+
+std::string URL::ToFilePath() const {
+  if (context_.scheme != "file:") {
+    return "";
+  }
+
+#ifdef _WIN32
+  const char* slash = "\\";
+  auto is_slash = [] (char ch) {
+    return ch == '/' || ch == '\\';
+  };
+#else
+  const char* slash = "/";
+  auto is_slash = [] (char ch) {
+    return ch == '/';
+  };
+  if ((context_.flags & URL_FLAGS_HAS_HOST) &&
+      context_.host.length() > 0) {
+    return "";
+  }
+#endif
+  std::string decoded_path;
+  for (const std::string& part : context_.path) {
+    std::string decoded;
+    PercentDecode(part.c_str(), part.length(), &decoded);
+    for (char& ch : decoded) {
+      if (is_slash(ch)) {
+        return "";
+      }
+    }
+    decoded_path += slash + decoded;
+  }
+
+#ifdef _WIN32
+  // TODO(TimothyGu): Use "\\?\" long paths on Windows.
+
+  // If hostname is set, then we have a UNC path. Pass the hostname through
+  // ToUnicode just in case it is an IDN using punycode encoding. We do not
+  // need to worry about percent encoding because the URL parser will have
+  // already taken care of that for us. Note that this only causes IDNs with an
+  // appropriate `xn--` prefix to be decoded.
+  if ((context_.flags & URL_FLAGS_HAS_HOST) &&
+      context_.host.length() > 0) {
+    std::string unicode_host;
+    if (!ToUnicode(context_.host, &unicode_host)) {
+      return "";
+    }
+    return "\\\\" + unicode_host + decoded_path;
+  }
+  // Otherwise, it's a local path that requires a drive letter.
+  if (decoded_path.length() < 3) {
+    return "";
+  }
+  if (decoded_path[2] != ':' ||
+      !IsASCIIAlpha(decoded_path[1])) {
+    return "";
+  }
+  // Strip out the leading '\'.
+  return decoded_path.substr(1);
+#else
+  return decoded_path;
+#endif
+}
+
+// This function works by calling out to a JS function that creates and
+// returns the JS URL object. Be mindful of the JS<->Native boundary
+// crossing that is required.
+const Local<Value> URL::ToObject(Environment* env) const {
+  Isolate* isolate = env->isolate();
+  Local<Context> context = env->context();
+  Context::Scope context_scope(context);
+
+  const Local<Value> undef = Undefined(isolate);
+  const Local<Value> null = Null(isolate);
+
+  if (context_.flags & URL_FLAGS_FAILED)
+    return Local<Value>();
+
+  Local<Value> argv[9] = {
+    undef,
+    undef,
+    undef,
+    undef,
+    null,  // host defaults to null
+    null,  // port defaults to null
+    undef,
+    null,  // query defaults to null
+    null,  // fragment defaults to null
+  };
+  SetArgs(env, argv, &context_);
+
+  TryCatch try_catch(isolate);
+
+  // The SetURLConstructor method must have been called already to
+  // set the constructor function used below. SetURLConstructor is
+  // called automatically when the internal/url.js module is loaded
+  // during the internal/bootstrap_node.js processing.
+  MaybeLocal<Value> ret =
+      env->url_constructor_function()
+          ->Call(env->context(), undef, 9, argv);
+
+  if (ret.IsEmpty()) {
+    ClearFatalExceptionHandlers(env);
+    FatalException(isolate, try_catch);
+  }
+
+  return ret.ToLocalChecked();
+}
+
+static void SetURLConstructor(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  CHECK_EQ(args.Length(), 1);
+  CHECK(args[0]->IsFunction());
+  env->set_url_constructor_function(args[0].As<Function>());
+}
+
+static void Init(Local<Object> target,
+                 Local<Value> unused,
+                 Local<Context> context,
+                 void* priv) {
+  Environment* env = Environment::GetCurrent(context);
+  env->SetMethod(target, "parse", Parse);
+  env->SetMethod(target, "encodeAuth", EncodeAuthSet);
+  env->SetMethod(target, "toUSVString", ToUSVString);
+  env->SetMethod(target, "domainToASCII", DomainToASCII);
+  env->SetMethod(target, "domainToUnicode", DomainToUnicode);
+  env->SetMethod(target, "setURLConstructor", SetURLConstructor);
+
+#define XX(name, _) NODE_DEFINE_CONSTANT(target, name);
+  FLAGS(XX)
+#undef XX
+
+#define XX(name) NODE_DEFINE_CONSTANT(target, name);
+  PARSESTATES(XX)
+#undef XX
+}
+}  // namespace url
+}  // namespace node
+
+NODE_MODULE_CONTEXT_AWARE_BUILTIN(url, node::url::Init)
diff --git a/src/node_url.h b/src/node_url.h
new file mode 100644
index 00000000000000..6b526d15b07703
--- /dev/null
+++ b/src/node_url.h
@@ -0,0 +1,191 @@
+#ifndef SRC_NODE_URL_H_
+#define SRC_NODE_URL_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include "node.h"
+#include "env-inl.h"
+
+#include <string>
+
+namespace node {
+namespace url {
+
+using v8::Local;
+using v8::Value;
+
+
+#define PARSESTATES(XX)                                                       \
+  XX(kSchemeStart)                                                            \
+  XX(kScheme)                                                                 \
+  XX(kNoScheme)                                                               \
+  XX(kSpecialRelativeOrAuthority)                                             \
+  XX(kPathOrAuthority)                                                        \
+  XX(kRelative)                                                               \
+  XX(kRelativeSlash)                                                          \
+  XX(kSpecialAuthoritySlashes)                                                \
+  XX(kSpecialAuthorityIgnoreSlashes)                                          \
+  XX(kAuthority)                                                              \
+  XX(kHost)                                                                   \
+  XX(kHostname)                                                               \
+  XX(kPort)                                                                   \
+  XX(kFile)                                                                   \
+  XX(kFileSlash)                                                              \
+  XX(kFileHost)                                                               \
+  XX(kPathStart)                                                              \
+  XX(kPath)                                                                   \
+  XX(kCannotBeBase)                                                           \
+  XX(kQuery)                                                                  \
+  XX(kFragment)
+
+#define FLAGS(XX)                                                             \
+  XX(URL_FLAGS_NONE, 0)                                                       \
+  XX(URL_FLAGS_FAILED, 0x01)                                                  \
+  XX(URL_FLAGS_CANNOT_BE_BASE, 0x02)                                          \
+  XX(URL_FLAGS_INVALID_PARSE_STATE, 0x04)                                     \
+  XX(URL_FLAGS_TERMINATED, 0x08)                                              \
+  XX(URL_FLAGS_SPECIAL, 0x10)                                                 \
+  XX(URL_FLAGS_HAS_USERNAME, 0x20)                                            \
+  XX(URL_FLAGS_HAS_PASSWORD, 0x40)                                            \
+  XX(URL_FLAGS_HAS_HOST, 0x80)                                                \
+  XX(URL_FLAGS_HAS_PATH, 0x100)                                               \
+  XX(URL_FLAGS_HAS_QUERY, 0x200)                                              \
+  XX(URL_FLAGS_HAS_FRAGMENT, 0x400)
+
+enum url_parse_state {
+  kUnknownState = -1,
+#define XX(name) name,
+  PARSESTATES(XX)
+#undef XX
+};
+
+enum url_flags {
+#define XX(name, val) name = val,
+  FLAGS(XX)
+#undef XX
+};
+
+struct url_data {
+  int32_t flags = URL_FLAGS_NONE;
+  int port = -1;
+  std::string scheme;
+  std::string username;
+  std::string password;
+  std::string host;
+  std::string query;
+  std::string fragment;
+  std::vector<std::string> path;
+};
+
+class URL {
+ public:
+  static void Parse(const char* input,
+                    size_t len,
+                    enum url_parse_state state_override,
+                    struct url_data* url,
+                    bool has_url,
+                    const struct url_data* base,
+                    bool has_base);
+
+  URL(const char* input, const size_t len) {
+    Parse(input, len, kUnknownState, &context_, false, nullptr, false);
+  }
+
+  URL(const char* input, const size_t len, const URL* base) {
+    if (base != nullptr)
+      Parse(input, len, kUnknownState,
+            &context_, false,
+            &(base->context_), true);
+    else
+      Parse(input, len, kUnknownState, &context_, false, nullptr, false);
+  }
+
+  URL(const char* input, const size_t len,
+      const char* base, const size_t baselen) {
+    if (base != nullptr && baselen > 0) {
+      URL _base(base, baselen);
+      Parse(input, len, kUnknownState,
+            &context_, false,
+            &(_base.context_), true);
+    } else {
+      Parse(input, len, kUnknownState, &context_, false, nullptr, false);
+    }
+  }
+
+  explicit URL(std::string input) :
+      URL(input.c_str(), input.length()) {}
+
+  URL(std::string input, const URL* base) :
+      URL(input.c_str(), input.length(), base) {}
+
+  URL(std::string input, const URL& base) :
+      URL(input.c_str(), input.length(), &base) {}
+
+  URL(std::string input, std::string base) :
+      URL(input.c_str(), input.length(), base.c_str(), base.length()) {}
+
+  int32_t flags() {
+    return context_.flags;
+  }
+
+  int port() {
+    return context_.port;
+  }
+
+  const std::string& protocol() const {
+    return context_.scheme;
+  }
+
+  const std::string& username() const {
+    return context_.username;
+  }
+
+  const std::string& password() const {
+    return context_.password;
+  }
+
+  const std::string& host() const {
+    return context_.host;
+  }
+
+  const std::string& query() const {
+    return context_.query;
+  }
+
+  const std::string& fragment() const {
+    return context_.fragment;
+  }
+
+  std::string path() const {
+    std::string ret;
+    for (auto i = context_.path.begin(); i != context_.path.end(); i++) {
+      ret += '/';
+      ret += *i;
+    }
+    return ret;
+  }
+
+  // Get the path of the file: URL in a format consumable by native file system
+  // APIs. Returns an empty string if something went wrong.
+  std::string ToFilePath() const;
+
+  const Local<Value> ToObject(Environment* env) const;
+
+  URL(const URL&) = default;
+  URL& operator=(const URL&) = default;
+  URL(URL&&) = default;
+  URL& operator=(URL&&) = default;
+
+  URL() : URL("") {}
+
+ private:
+  struct url_data context_;
+};
+
+}  // namespace url
+
+}  // namespace node
+
+#endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#endif  // SRC_NODE_URL_H_
diff --git a/src/util.h b/src/util.h
index 4ce25e4622f4b2..d1ada38ed57fd2 100644
--- a/src/util.h
+++ b/src/util.h
@@ -10,6 +10,7 @@
 #include <stddef.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 
 // OSX 10.9 defaults to libc++ which provides a C++11 <type_traits> header.
 #if defined(__APPLE__) && __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ < 1090
@@ -310,29 +311,40 @@ class MaybeStackBuffer {
     return length_;
   }
 
-  // Call to make sure enough space for `storage` entries is available.
-  // There can only be 1 call to AllocateSufficientStorage or Invalidate
-  // per instance.
+  // Current maximum capacity of the buffer with which SetLength() can be used
+  // without first calling AllocateSufficientStorage().
+  size_t capacity() const {
+    return IsAllocated() ? capacity_ :
+                           IsInvalidated() ? 0 : kStackStorageSize;
+  }
+
+  // Make sure enough space for `storage` entries is available.
+  // This method can be called multiple times throughout the lifetime of the
+  // buffer, but once this has been called Invalidate() cannot be used.
+  // Content of the buffer in the range [0, length()) is preserved.
   void AllocateSufficientStorage(size_t storage) {
-    if (storage <= kStackStorageSize) {
-      buf_ = buf_st_;
-    } else {
-      buf_ = Malloc<T>(storage);
+    CHECK(!IsInvalidated());
+    if (storage > capacity()) {
+      bool was_allocated = IsAllocated();
+      T* allocated_ptr = was_allocated ? buf_ : nullptr;
+      buf_ = Realloc(allocated_ptr, storage);
+      capacity_ = storage;
+      if (!was_allocated && length_ > 0)
+        memcpy(buf_, buf_st_, length_ * sizeof(buf_[0]));
     }
 
-    // Remember how much was allocated to check against that in SetLength().
     length_ = storage;
   }
 
   void SetLength(size_t length) {
-    // length_ stores how much memory was allocated.
-    CHECK_LE(length, length_);
+    // capacity() returns how much memory is actually available.
+    CHECK_LE(length, capacity());
     length_ = length;
   }
 
   void SetLengthAndZeroTerminate(size_t length) {
-    // length_ stores how much memory was allocated.
-    CHECK_LE(length + 1, length_);
+    // capacity() returns how much memory is actually available.
+    CHECK_LE(length + 1, capacity());
     SetLength(length);
 
     // T() is 0 for integer types, nullptr for pointers, etc.
@@ -340,15 +352,35 @@ class MaybeStackBuffer {
   }
 
   // Make derefencing this object return nullptr.
-  // Calling this is mutually exclusive with calling
-  // AllocateSufficientStorage.
+  // This method can be called multiple times throughout the lifetime of the
+  // buffer, but once this has been called AllocateSufficientStorage() cannot
+  // be used.
   void Invalidate() {
-    CHECK_EQ(buf_, buf_st_);
+    CHECK(!IsAllocated());
     length_ = 0;
     buf_ = nullptr;
   }
 
-  MaybeStackBuffer() : length_(0), buf_(buf_st_) {
+  // If the buffer is stored in the heap rather than on the stack.
+  bool IsAllocated() const {
+    return !IsInvalidated() && buf_ != buf_st_;
+  }
+
+  // If Invalidate() has been called.
+  bool IsInvalidated() const {
+    return buf_ == nullptr;
+  }
+
+  // Release ownership of the malloc'd buffer.
+  // Note: This does not free the buffer.
+  void Release() {
+    CHECK(IsAllocated());
+    buf_ = buf_st_;
+    length_ = 0;
+    capacity_ = 0;
+  }
+
+  MaybeStackBuffer() : length_(0), capacity_(0), buf_(buf_st_) {
     // Default to a zero-length, null-terminated buffer.
     buf_[0] = T();
   }
@@ -358,12 +390,14 @@ class MaybeStackBuffer {
   }
 
   ~MaybeStackBuffer() {
-    if (buf_ != buf_st_)
+    if (IsAllocated())
       free(buf_);
   }
 
  private:
   size_t length_;
+  // capacity of the malloc'ed buf_
+  size_t capacity_;
   T* buf_;
   T buf_st_[kStackStorageSize];
 };
diff --git a/test/cctest/test_url.cc b/test/cctest/test_url.cc
new file mode 100644
index 00000000000000..0b80d44caad807
--- /dev/null
+++ b/test/cctest/test_url.cc
@@ -0,0 +1,106 @@
+#include "node_url.h"
+#include "node_i18n.h"
+
+#include "gtest/gtest.h"
+
+using node::url::URL;
+using node::url::URL_FLAGS_FAILED;
+
+class URLTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+#if defined(NODE_HAVE_I18N_SUPPORT)
+    std::string icu_data_dir;
+    node::i18n::InitializeICUDirectory(icu_data_dir);
+#endif
+  }
+
+  void TearDown() override {}
+};
+
+TEST_F(URLTest, Simple) {
+  URL simple("https://example.org:81/a/b/c?query#fragment");
+
+  EXPECT_FALSE(simple.flags() & URL_FLAGS_FAILED);
+  EXPECT_EQ(simple.protocol(), "https:");
+  EXPECT_EQ(simple.host(), "example.org");
+  EXPECT_EQ(simple.port(), 81);
+  EXPECT_EQ(simple.path(), "/a/b/c");
+  EXPECT_EQ(simple.query(), "query");
+  EXPECT_EQ(simple.fragment(), "fragment");
+}
+
+TEST_F(URLTest, Simple2) {
+  const char* input = "https://example.org:81/a/b/c?query#fragment";
+  URL simple(input, strlen(input));
+
+  EXPECT_FALSE(simple.flags() & URL_FLAGS_FAILED);
+  EXPECT_EQ(simple.protocol(), "https:");
+  EXPECT_EQ(simple.host(), "example.org");
+  EXPECT_EQ(simple.port(), 81);
+  EXPECT_EQ(simple.path(), "/a/b/c");
+  EXPECT_EQ(simple.query(), "query");
+  EXPECT_EQ(simple.fragment(), "fragment");
+}
+
+TEST_F(URLTest, NoBase1) {
+  URL error("123noscheme");
+  EXPECT_TRUE(error.flags() & URL_FLAGS_FAILED);
+}
+
+TEST_F(URLTest, Base1) {
+  URL base("http://example.org/foo/bar");
+  ASSERT_FALSE(base.flags() & URL_FLAGS_FAILED);
+
+  URL simple("../baz", &base);
+  EXPECT_FALSE(simple.flags() & URL_FLAGS_FAILED);
+  EXPECT_EQ(simple.protocol(), "http:");
+  EXPECT_EQ(simple.host(), "example.org");
+  EXPECT_EQ(simple.path(), "/baz");
+}
+
+TEST_F(URLTest, Base2) {
+  URL simple("../baz", "http://example.org/foo/bar");
+
+  EXPECT_FALSE(simple.flags() & URL_FLAGS_FAILED);
+  EXPECT_EQ(simple.protocol(), "http:");
+  EXPECT_EQ(simple.host(), "example.org");
+  EXPECT_EQ(simple.path(), "/baz");
+}
+
+TEST_F(URLTest, Base3) {
+  const char* input = "../baz";
+  const char* base = "http://example.org/foo/bar";
+
+  URL simple(input, strlen(input), base, strlen(base));
+
+  EXPECT_FALSE(simple.flags() & URL_FLAGS_FAILED);
+  EXPECT_EQ(simple.protocol(), "http:");
+  EXPECT_EQ(simple.host(), "example.org");
+  EXPECT_EQ(simple.path(), "/baz");
+}
+
+TEST_F(URLTest, ToFilePath) {
+#define T(url, path) EXPECT_EQ(path, URL(url).ToFilePath())
+  T("http://example.org/foo/bar", "");
+
+#ifdef _WIN32
+  T("file:///C:/Program%20Files/", "C:\\Program Files\\");
+  T("file:///C:/a/b/c?query#fragment", "C:\\a\\b\\c");
+  T("file://host/path/a/b/c?query#fragment", "\\\\host\\path\\a\\b\\c");
+  T("file://xn--weird-prdj8vva.com/host/a", "\\\\wͪ͊eiͬ͋rd.com\\host\\a");
+  T("file:///C:/a%2Fb", "");
+  T("file:///", "");
+  T("file:///home", "");
+#else
+  T("file:///", "/");
+  T("file:///home/user?query#fragment", "/home/user");
+  T("file:///home/user/?query#fragment", "/home/user/");
+  T("file:///home/user/%20space", "/home/user/ space");
+  T("file:///home/us%5Cer", "/home/us\\er");
+  T("file:///home/us%2Fer", "");
+  T("file://host/path", "");
+#endif
+
+#undef T
+}
diff --git a/test/cctest/test_util.cc b/test/cctest/test_util.cc
index f1446ae0345153..62309306ba6ffb 100644
--- a/test/cctest/test_util.cc
+++ b/test/cctest/test_util.cc
@@ -121,3 +121,127 @@ TEST(UtilTest, UncheckedCalloc) {
   EXPECT_NE(nullptr, UncheckedCalloc(0));
   EXPECT_NE(nullptr, UncheckedCalloc(1));
 }
+
+template <typename T>
+static void MaybeStackBufferBasic() {
+  using node::MaybeStackBuffer;
+
+  MaybeStackBuffer<T> buf;
+  size_t old_length;
+  size_t old_capacity;
+
+  /* Default constructor */
+  EXPECT_EQ(0U, buf.length());
+  EXPECT_FALSE(buf.IsAllocated());
+  EXPECT_GT(buf.capacity(), buf.length());
+
+  /* SetLength() expansion */
+  buf.SetLength(buf.capacity());
+  EXPECT_EQ(buf.capacity(), buf.length());
+  EXPECT_FALSE(buf.IsAllocated());
+
+  /* Means of accessing raw buffer */
+  EXPECT_EQ(buf.out(), *buf);
+  EXPECT_EQ(&buf[0], *buf);
+
+  /* Basic I/O */
+  for (size_t i = 0; i < buf.length(); i++)
+    buf[i] = static_cast<T>(i);
+  for (size_t i = 0; i < buf.length(); i++)
+    EXPECT_EQ(static_cast<T>(i), buf[i]);
+
+  /* SetLengthAndZeroTerminate() */
+  buf.SetLengthAndZeroTerminate(buf.capacity() - 1);
+  EXPECT_EQ(buf.capacity() - 1, buf.length());
+  for (size_t i = 0; i < buf.length(); i++)
+    EXPECT_EQ(static_cast<T>(i), buf[i]);
+  buf.SetLength(buf.capacity());
+  EXPECT_EQ(0, buf[buf.length() - 1]);
+
+  /* Initial Realloc */
+  old_length = buf.length() - 1;
+  old_capacity = buf.capacity();
+  buf.AllocateSufficientStorage(buf.capacity() * 2);
+  EXPECT_EQ(buf.capacity(), buf.length());
+  EXPECT_TRUE(buf.IsAllocated());
+  for (size_t i = 0; i < old_length; i++)
+    EXPECT_EQ(static_cast<T>(i), buf[i]);
+  EXPECT_EQ(0, buf[old_length]);
+
+  /* SetLength() reduction and expansion */
+  for (size_t i = 0; i < buf.length(); i++)
+    buf[i] = static_cast<T>(i);
+  buf.SetLength(10);
+  for (size_t i = 0; i < buf.length(); i++)
+    EXPECT_EQ(static_cast<T>(i), buf[i]);
+  buf.SetLength(buf.capacity());
+  for (size_t i = 0; i < buf.length(); i++)
+    EXPECT_EQ(static_cast<T>(i), buf[i]);
+
+  /* Subsequent Realloc */
+  old_length = buf.length();
+  old_capacity = buf.capacity();
+  buf.AllocateSufficientStorage(old_capacity * 1.5);
+  EXPECT_EQ(buf.capacity(), buf.length());
+  EXPECT_EQ(static_cast<size_t>(old_capacity * 1.5), buf.length());
+  EXPECT_TRUE(buf.IsAllocated());
+  for (size_t i = 0; i < old_length; i++)
+    EXPECT_EQ(static_cast<T>(i), buf[i]);
+
+  /* Basic I/O on Realloc'd buffer */
+  for (size_t i = 0; i < buf.length(); i++)
+    buf[i] = static_cast<T>(i);
+  for (size_t i = 0; i < buf.length(); i++)
+    EXPECT_EQ(static_cast<T>(i), buf[i]);
+
+  /* Release() */
+  T* rawbuf = buf.out();
+  buf.Release();
+  EXPECT_EQ(0U, buf.length());
+  EXPECT_FALSE(buf.IsAllocated());
+  EXPECT_GT(buf.capacity(), buf.length());
+  free(rawbuf);
+}
+
+TEST(UtilTest, MaybeStackBuffer) {
+  using node::MaybeStackBuffer;
+
+  MaybeStackBufferBasic<uint8_t>();
+  MaybeStackBufferBasic<uint16_t>();
+
+  // Constructor with size parameter
+  {
+    MaybeStackBuffer<unsigned char> buf(100);
+    EXPECT_EQ(100U, buf.length());
+    EXPECT_FALSE(buf.IsAllocated());
+    EXPECT_GT(buf.capacity(), buf.length());
+    buf.SetLength(buf.capacity());
+    EXPECT_EQ(buf.capacity(), buf.length());
+    EXPECT_FALSE(buf.IsAllocated());
+    for (size_t i = 0; i < buf.length(); i++)
+      buf[i] = static_cast<unsigned char>(i);
+    for (size_t i = 0; i < buf.length(); i++)
+      EXPECT_EQ(static_cast<unsigned char>(i), buf[i]);
+
+    MaybeStackBuffer<unsigned char> bigbuf(10000);
+    EXPECT_EQ(10000U, bigbuf.length());
+    EXPECT_TRUE(bigbuf.IsAllocated());
+    EXPECT_EQ(bigbuf.length(), bigbuf.capacity());
+    for (size_t i = 0; i < bigbuf.length(); i++)
+      bigbuf[i] = static_cast<unsigned char>(i);
+    for (size_t i = 0; i < bigbuf.length(); i++)
+      EXPECT_EQ(static_cast<unsigned char>(i), bigbuf[i]);
+  }
+
+  // Invalidated buffer
+  {
+    MaybeStackBuffer<char> buf;
+    buf.Invalidate();
+    EXPECT_TRUE(buf.IsInvalidated());
+    EXPECT_FALSE(buf.IsAllocated());
+    EXPECT_EQ(0U, buf.length());
+    EXPECT_EQ(0U, buf.capacity());
+    buf.Invalidate();
+    EXPECT_TRUE(buf.IsInvalidated());
+  }
+}
diff --git a/test/common/index.js b/test/common/index.js
index 148dad20001b04..b854c8b97725b1 100644
--- a/test/common/index.js
+++ b/test/common/index.js
@@ -577,6 +577,12 @@ exports.expectWarning = function(name, expected) {
   }, expected.length));
 };
 
+Object.defineProperty(exports, 'hasIntl', {
+  get: function() {
+    return process.binding('config').hasIntl;
+  }
+});
+
 // Crash the process on unhandled rejections.
 exports.crashOnUnhandledRejection = function() {
   process.on('unhandledRejection',
diff --git a/test/fixtures/url-idna.js b/test/fixtures/url-idna.js
new file mode 100644
index 00000000000000..4b8f5a48cc9646
--- /dev/null
+++ b/test/fixtures/url-idna.js
@@ -0,0 +1,215 @@
+'use strict';
+
+// Credit for list: http://www.i18nguy.com/markup/idna-examples.html
+module.exports = [
+  { ascii: 'xn--mgbaal8b0b9b2b.icom.museum',
+    unicode: 'افغانستا.icom.museum'
+  },
+  {
+    ascii: 'xn--lgbbat1ad8j.icom.museum',
+    unicode: 'الجزائر.icom.museum'
+  },
+  {
+    ascii: 'xn--sterreich-z7a.icom.museum',
+    unicode: 'österreich.icom.museum'
+  },
+  {
+    ascii: 'xn--54b6eqazv8bc7e.icom.museum',
+    unicode: 'বাংলাদেশ.icom.museum'
+  },
+  {
+    ascii: 'xn--80abmy0agn7e.icom.museum',
+    unicode: 'беларусь.icom.museum'
+  },
+  {
+    ascii: 'xn--belgi-rsa.icom.museum',
+    unicode: 'belgië.icom.museum'
+  },
+  {
+    ascii: 'xn--80abgvm6a7d2b.icom.museum',
+    unicode: 'българия.icom.museum'
+  },
+  {
+    ascii: 'xn--mgbfqim.icom.museum',
+    unicode: 'تشادر.icom.museum'
+  },
+  {
+    ascii: 'xn--fiqs8s.icom.museum',
+    unicode: '中国.icom.museum'
+  },
+  {
+    ascii: 'xn--mgbu4chg.icom.museum',
+    unicode: 'القمر.icom.museum'
+  },
+  {
+    ascii: 'xn--vxakcego.icom.museum',
+    unicode: 'κυπρος.icom.museum'
+  },
+  {
+    ascii: 'xn--eskrepublika-ebb62d.icom.museum',
+    unicode: 'českárepublika.icom.museum'
+  },
+  {
+    ascii: 'xn--wgbh1c.icom.museum',
+    unicode: 'مصر.icom.museum'
+  },
+  {
+    ascii: 'xn--hxakic4aa.icom.museum',
+    unicode: 'ελλάδα.icom.museum'
+  },
+  {
+    ascii: 'xn--magyarorszg-t7a.icom.museum',
+    unicode: 'magyarország.icom.museum'
+  },
+  {
+    ascii: 'xn--sland-ysa.icom.museum',
+    unicode: 'ísland.icom.museum'
+  },
+  {
+    ascii: 'xn--h2brj9c.icom.museum',
+    unicode: 'भारत.icom.museum'
+  },
+  {
+    ascii: 'xn--mgba3a4fra.icom.museum',
+    unicode: 'ايران.icom.museum'
+  },
+  {
+    ascii: 'xn--ire-9la.icom.museum',
+    unicode: 'éire.icom.museum'
+  },
+  {
+    ascii: 'xn--4dbklr2c8d.xn--4dbrk0ce.museum',
+    unicode: 'איקו״ם.ישראל.museum'
+  },
+  {
+    ascii: 'xn--wgv71a.icom.museum',
+    unicode: '日本.icom.museum'
+  },
+  {
+    ascii: 'xn--igbhzh7gpa.icom.museum',
+    unicode: 'الأردن.icom.museum'
+  },
+  {
+    ascii: 'xn--80aaa0a6awh12ed.icom.museum',
+    unicode: 'қазақстан.icom.museum'
+  },
+  {
+    ascii: 'xn--3e0b707e.icom.museum',
+    unicode: '한국.icom.museum'
+  },
+  {
+    ascii: 'xn--80afmksoji0fc.icom.museum',
+    unicode: 'кыргызстан.icom.museum'
+  },
+  {
+    ascii: 'xn--q7ce6a.icom.museum',
+    unicode: 'ລາວ.icom.museum'
+  },
+  {
+    ascii: 'xn--mgbb7fjb.icom.museum',
+    unicode: 'لبنان.icom.museum'
+  },
+  {
+    ascii: 'xn--80aaldqjmmi6x.icom.museum',
+    unicode: 'македонија.icom.museum'
+  },
+  {
+    ascii: 'xn--mgbah1a3hjkrd.icom.museum',
+    unicode: 'موريتانيا.icom.museum'
+  },
+  {
+    ascii: 'xn--mxico-bsa.icom.museum',
+    unicode: 'méxico.icom.museum'
+  },
+  {
+    ascii: 'xn--c1aqabffc0aq.icom.museum',
+    unicode: 'монголулс.icom.museum'
+  },
+  {
+    ascii: 'xn--mgbc0a9azcg.icom.museum',
+    unicode: 'المغرب.icom.museum'
+  },
+  {
+    ascii: 'xn--l2bey1c2b.icom.museum',
+    unicode: 'नेपाल.icom.museum'
+  },
+  {
+    ascii: 'xn--mgb9awbf.icom.museum',
+    unicode: 'عمان.icom.museum'
+  },
+  {
+    ascii: 'xn--wgbl6a.icom.museum',
+    unicode: 'قطر.icom.museum'
+  },
+  {
+    ascii: 'xn--romnia-yta.icom.museum',
+    unicode: 'românia.icom.museum'
+  },
+  {
+    ascii: 'xn--h1alffa9f.xn--h1aegh.museum',
+    unicode: 'россия.иком.museum'
+  },
+  {
+    ascii: 'xn--80aaabm1ab4blmeec9e7n.xn--h1aegh.museum',
+    unicode: 'србијаицрнагора.иком.museum'
+  },
+  {
+    ascii: 'xn--xkc2al3hye2a.icom.museum',
+    unicode: 'இலங்கை.icom.museum'
+  },
+  {
+    ascii: 'xn--espaa-rta.icom.museum',
+    unicode: 'españa.icom.museum'
+  },
+  {
+    ascii: 'xn--o3cw4h.icom.museum',
+    unicode: 'ไทย.icom.museum'
+  },
+  {
+    ascii: 'xn--pgbs0dh.icom.museum',
+    unicode: 'تونس.icom.museum'
+  },
+  {
+    ascii: 'xn--trkiye-3ya.icom.museum',
+    unicode: 'türkiye.icom.museum'
+  },
+  {
+    ascii: 'xn--80aaxgrpt.icom.museum',
+    unicode: 'украина.icom.museum'
+  },
+  {
+    ascii: 'xn--vitnam-jk8b.icom.museum',
+    unicode: 'việtnam.icom.museum'
+  },
+  // long label
+  {
+    ascii: `${'a'.repeat(64)}.com`,
+    unicode: `${'a'.repeat(64)}.com`,
+  },
+  // long URL
+  {
+    ascii: `${`${'a'.repeat(64)}.`.repeat(4)}com`,
+    unicode: `${`${'a'.repeat(64)}.`.repeat(4)}com`
+  },
+  // URLs with hyphen
+  {
+    ascii: 'r4---sn-a5mlrn7s.gevideo.com',
+    unicode: 'r4---sn-a5mlrn7s.gevideo.com'
+  },
+  {
+    ascii: '-sn-a5mlrn7s.gevideo.com',
+    unicode: '-sn-a5mlrn7s.gevideo.com'
+  },
+  {
+    ascii: 'sn-a5mlrn7s-.gevideo.com',
+    unicode: 'sn-a5mlrn7s-.gevideo.com'
+  },
+  {
+    ascii: '-sn-a5mlrn7s-.gevideo.com',
+    unicode: '-sn-a5mlrn7s-.gevideo.com'
+  },
+  {
+    ascii: '-sn--a5mlrn7s-.gevideo.com',
+    unicode: '-sn--a5mlrn7s-.gevideo.com'
+  }
+];
diff --git a/test/fixtures/url-searchparams.js b/test/fixtures/url-searchparams.js
new file mode 100644
index 00000000000000..918af678bc563e
--- /dev/null
+++ b/test/fixtures/url-searchparams.js
@@ -0,0 +1,77 @@
+module.exports = [
+  ['', '', []],
+  [
+    'foo=918854443121279438895193',
+    'foo=918854443121279438895193',
+    [['foo', '918854443121279438895193']]
+  ],
+  ['foo=bar', 'foo=bar', [['foo', 'bar']]],
+  ['foo=bar&foo=quux', 'foo=bar&foo=quux', [['foo', 'bar'], ['foo', 'quux']]],
+  ['foo=1&bar=2', 'foo=1&bar=2', [['foo', '1'], ['bar', '2']]],
+  [
+    "my%20weird%20field=q1!2%22'w%245%267%2Fz8)%3F",
+    'my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F',
+    [['my weird field', 'q1!2"\'w$5&7/z8)?']]
+  ],
+  ['foo%3Dbaz=bar', 'foo%3Dbaz=bar', [['foo=baz', 'bar']]],
+  ['foo=baz=bar', 'foo=baz%3Dbar', [['foo', 'baz=bar']]],
+  [
+    'str=foo&arr=1&somenull&arr=2&undef=&arr=3',
+    'str=foo&arr=1&somenull=&arr=2&undef=&arr=3',
+    [
+      ['str', 'foo'],
+      ['arr', '1'],
+      ['somenull', ''],
+      ['arr', '2'],
+      ['undef', ''],
+      ['arr', '3']
+    ]
+  ],
+  [' foo = bar ', '+foo+=+bar+', [[' foo ', ' bar ']]],
+  ['foo=%zx', 'foo=%25zx', [['foo', '%zx']]],
+  ['foo=%EF%BF%BD', 'foo=%EF%BF%BD', [['foo', '\ufffd']]],
+  // See: https://github.com/joyent/node/issues/3058
+  ['foo&bar=baz', 'foo=&bar=baz', [['foo', ''], ['bar', 'baz']]],
+  ['a=b&c&d=e', 'a=b&c=&d=e', [['a', 'b'], ['c', ''], ['d', 'e']]],
+  ['a=b&c=&d=e', 'a=b&c=&d=e', [['a', 'b'], ['c', ''], ['d', 'e']]],
+  ['a=b&=c&d=e', 'a=b&=c&d=e', [['a', 'b'], ['', 'c'], ['d', 'e']]],
+  ['a=b&=&d=e', 'a=b&=&d=e', [['a', 'b'], ['', ''], ['d', 'e']]],
+  ['&&foo=bar&&', 'foo=bar', [['foo', 'bar']]],
+  ['&', '', []],
+  ['&&&&', '', []],
+  ['&=&', '=', [['', '']]],
+  ['&=&=', '=&=', [['', ''], ['', '']]],
+  ['=', '=', [['', '']]],
+  ['+', '+=', [[' ', '']]],
+  ['+=', '+=', [[' ', '']]],
+  ['+&', '+=', [[' ', '']]],
+  ['=+', '=+', [['', ' ']]],
+  ['+=&', '+=', [[' ', '']]],
+  ['a&&b', 'a=&b=', [['a', ''], ['b', '']]],
+  ['a=a&&b=b', 'a=a&b=b', [['a', 'a'], ['b', 'b']]],
+  ['&a', 'a=', [['a', '']]],
+  ['&=', '=', [['', '']]],
+  ['a&a&', 'a=&a=', [['a', ''], ['a', '']]],
+  ['a&a&a&', 'a=&a=&a=', [['a', ''], ['a', ''], ['a', '']]],
+  ['a&a&a&a&', 'a=&a=&a=&a=', [['a', ''], ['a', ''], ['a', ''], ['a', '']]],
+  ['a=&a=value&a=', 'a=&a=value&a=', [['a', ''], ['a', 'value'], ['a', '']]],
+  ['foo%20bar=baz%20quux', 'foo+bar=baz+quux', [['foo bar', 'baz quux']]],
+  ['+foo=+bar', '+foo=+bar', [[' foo', ' bar']]],
+  ['a+', 'a+=', [['a ', '']]],
+  ['=a+', '=a+', [['', 'a ']]],
+  ['a+&', 'a+=', [['a ', '']]],
+  ['=a+&', '=a+', [['', 'a ']]],
+  ['%20+', '++=', [['  ', '']]],
+  ['=%20+', '=++', [['', '  ']]],
+  ['%20+&', '++=', [['  ', '']]],
+  ['=%20+&', '=++', [['', '  ']]],
+  [
+    // fake percent encoding
+    'foo=%©ar&baz=%A©uux&xyzzy=%©ud',
+    'foo=%25%C2%A9ar&baz=%25A%C2%A9uux&xyzzy=%25%C2%A9ud',
+    [['foo', '%©ar'], ['baz', '%A©uux'], ['xyzzy', '%©ud']]
+  ],
+  // always preserve order of key-value pairs
+  ['a=1&b=2&a=3', 'a=1&b=2&a=3', [['a', '1'], ['b', '2'], ['a', '3']]],
+  ['?a', '%3Fa=', [['?a', '']]]
+];
diff --git a/test/fixtures/url-setter-tests-additional.js b/test/fixtures/url-setter-tests-additional.js
new file mode 100644
index 00000000000000..b27ae336a28776
--- /dev/null
+++ b/test/fixtures/url-setter-tests-additional.js
@@ -0,0 +1,237 @@
+module.exports = {
+  'username': [
+    {
+      'comment': 'Surrogate pair',
+      'href': 'https://github.com/',
+      'new_value': '\uD83D\uDE00',
+      'expected': {
+        'href': 'https://%F0%9F%98%80@github.com/',
+        'username': '%F0%9F%98%80'
+      }
+    },
+    {
+      'comment': 'Unpaired low surrogate 1',
+      'href': 'https://github.com/',
+      'new_value': '\uD83D',
+      'expected': {
+        'href': 'https://%EF%BF%BD@github.com/',
+        'username': '%EF%BF%BD'
+      }
+    },
+    {
+      'comment': 'Unpaired low surrogate 2',
+      'href': 'https://github.com/',
+      'new_value': '\uD83Dnode',
+      'expected': {
+        'href': 'https://%EF%BF%BDnode@github.com/',
+        'username': '%EF%BF%BDnode'
+      }
+    },
+    {
+      'comment': 'Unpaired high surrogate 1',
+      'href': 'https://github.com/',
+      'new_value': '\uDE00',
+      'expected': {
+        'href': 'https://%EF%BF%BD@github.com/',
+        'username': '%EF%BF%BD'
+      }
+    },
+    {
+      'comment': 'Unpaired high surrogate 2',
+      'href': 'https://github.com/',
+      'new_value': '\uDE00node',
+      'expected': {
+        'href': 'https://%EF%BF%BDnode@github.com/',
+        'username': '%EF%BF%BDnode'
+      }
+    }
+  ],
+  'password': [
+    {
+      'comment': 'Surrogate pair',
+      'href': 'https://github.com/',
+      'new_value': '\uD83D\uDE00',
+      'expected': {
+        'href': 'https://:%F0%9F%98%80@github.com/',
+        'password': '%F0%9F%98%80'
+      }
+    },
+    {
+      'comment': 'Unpaired low surrogate 1',
+      'href': 'https://github.com/',
+      'new_value': '\uD83D',
+      'expected': {
+        'href': 'https://:%EF%BF%BD@github.com/',
+        'password': '%EF%BF%BD'
+      }
+    },
+    {
+      'comment': 'Unpaired low surrogate 2',
+      'href': 'https://github.com/',
+      'new_value': '\uD83Dnode',
+      'expected': {
+        'href': 'https://:%EF%BF%BDnode@github.com/',
+        'password': '%EF%BF%BDnode'
+      }
+    },
+    {
+      'comment': 'Unpaired high surrogate 1',
+      'href': 'https://github.com/',
+      'new_value': '\uDE00',
+      'expected': {
+        'href': 'https://:%EF%BF%BD@github.com/',
+        'password': '%EF%BF%BD'
+      }
+    },
+    {
+      'comment': 'Unpaired high surrogate 2',
+      'href': 'https://github.com/',
+      'new_value': '\uDE00node',
+      'expected': {
+        'href': 'https://:%EF%BF%BDnode@github.com/',
+        'password': '%EF%BF%BDnode'
+      }
+    }
+  ],
+  'pathname': [
+    {
+      'comment': 'Surrogate pair',
+      'href': 'https://github.com/',
+      'new_value': '/\uD83D\uDE00',
+      'expected': {
+        'href': 'https://github.com/%F0%9F%98%80',
+        'pathname': '/%F0%9F%98%80'
+      }
+    },
+    {
+      'comment': 'Unpaired low surrogate 1',
+      'href': 'https://github.com/',
+      'new_value': '/\uD83D',
+      'expected': {
+        'href': 'https://github.com/%EF%BF%BD',
+        'pathname': '/%EF%BF%BD'
+      }
+    },
+    {
+      'comment': 'Unpaired low surrogate 2',
+      'href': 'https://github.com/',
+      'new_value': '/\uD83Dnode',
+      'expected': {
+        'href': 'https://github.com/%EF%BF%BDnode',
+        'pathname': '/%EF%BF%BDnode'
+      }
+    },
+    {
+      'comment': 'Unpaired high surrogate 1',
+      'href': 'https://github.com/',
+      'new_value': '/\uDE00',
+      'expected': {
+        'href': 'https://github.com/%EF%BF%BD',
+        'pathname': '/%EF%BF%BD'
+      }
+    },
+    {
+      'comment': 'Unpaired high surrogate 2',
+      'href': 'https://github.com/',
+      'new_value': '/\uDE00node',
+      'expected': {
+        'href': 'https://github.com/%EF%BF%BDnode',
+        'pathname': '/%EF%BF%BDnode'
+      }
+    }
+  ],
+  'search': [
+    {
+      'comment': 'Surrogate pair',
+      'href': 'https://github.com/',
+      'new_value': '\uD83D\uDE00',
+      'expected': {
+        'href': 'https://github.com/?%F0%9F%98%80',
+        'search': '?%F0%9F%98%80'
+      }
+    },
+    {
+      'comment': 'Unpaired low surrogate 1',
+      'href': 'https://github.com/',
+      'new_value': '\uD83D',
+      'expected': {
+        'href': 'https://github.com/?%EF%BF%BD',
+        'search': '?%EF%BF%BD'
+      }
+    },
+    {
+      'comment': 'Unpaired low surrogate 2',
+      'href': 'https://github.com/',
+      'new_value': '\uD83Dnode',
+      'expected': {
+        'href': 'https://github.com/?%EF%BF%BDnode',
+        'search': '?%EF%BF%BDnode'
+      }
+    },
+    {
+      'comment': 'Unpaired high surrogate 1',
+      'href': 'https://github.com/',
+      'new_value': '\uDE00',
+      'expected': {
+        'href': 'https://github.com/?%EF%BF%BD',
+        'search': '?%EF%BF%BD'
+      }
+    },
+    {
+      'comment': 'Unpaired high surrogate 2',
+      'href': 'https://github.com/',
+      'new_value': '\uDE00node',
+      'expected': {
+        'href': 'https://github.com/?%EF%BF%BDnode',
+        'search': '?%EF%BF%BDnode'
+      }
+    }
+  ],
+  'hash': [
+    {
+      'comment': 'Surrogate pair',
+      'href': 'https://github.com/',
+      'new_value': '\uD83D\uDE00',
+      'expected': {
+        'href': 'https://github.com/#%F0%9F%98%80',
+        'hash': '#%F0%9F%98%80'
+      }
+    },
+    {
+      'comment': 'Unpaired low surrogate 1',
+      'href': 'https://github.com/',
+      'new_value': '\uD83D',
+      'expected': {
+        'href': 'https://github.com/#%EF%BF%BD',
+        'hash': '#%EF%BF%BD'
+      }
+    },
+    {
+      'comment': 'Unpaired low surrogate 2',
+      'href': 'https://github.com/',
+      'new_value': '\uD83Dnode',
+      'expected': {
+        'href': 'https://github.com/#%EF%BF%BDnode',
+        'hash': '#%EF%BF%BDnode'
+      }
+    },
+    {
+      'comment': 'Unpaired high surrogate 1',
+      'href': 'https://github.com/',
+      'new_value': '\uDE00',
+      'expected': {
+        'href': 'https://github.com/#%EF%BF%BD',
+        'hash': '#%EF%BF%BD'
+      }
+    },
+    {
+      'comment': 'Unpaired high surrogate 2',
+      'href': 'https://github.com/',
+      'new_value': '\uDE00node',
+      'expected': {
+        'href': 'https://github.com/#%EF%BF%BDnode',
+        'hash': '#%EF%BF%BDnode'
+      }
+    }
+  ]
+};
diff --git a/test/fixtures/url-setter-tests.js b/test/fixtures/url-setter-tests.js
new file mode 100644
index 00000000000000..6f769eaec7543d
--- /dev/null
+++ b/test/fixtures/url-setter-tests.js
@@ -0,0 +1,1823 @@
+'use strict';
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/b30abaecf4/url/setters_tests.json
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+module.exports =
+{
+    "comment": [
+        "## Tests for setters of https://url.spec.whatwg.org/#urlutils-members",
+        "",
+        "This file contains a JSON object.",
+        "Other than 'comment', each key is an attribute of the `URL` interface",
+        "defined in WHATWG’s URL Standard.",
+        "The values are arrays of test case objects for that attribute.",
+        "",
+        "To run a test case for the attribute `attr`:",
+        "",
+        "* Create a new `URL` object with the value for the 'href' key",
+        "  the constructor single parameter. (Without a base URL.)",
+        "  This must not throw.",
+        "* Set the attribute `attr` to (invoke its setter with)",
+        "  with the value of for 'new_value' key.",
+        "* The value for the 'expected' key is another object.",
+        "  For each `key` / `value` pair of that object,",
+        "  get the attribute `key` (invoke its getter).",
+        "  The returned string must be equal to `value`.",
+        "",
+        "Note: the 'href' setter is already covered by urltestdata.json."
+    ],
+    "protocol": [
+        {
+            "comment": "The empty string is not a valid scheme. Setter leaves the URL unchanged.",
+            "href": "a://example.net",
+            "new_value": "",
+            "expected": {
+                "href": "a://example.net",
+                "protocol": "a:"
+            }
+        },
+        {
+            "href": "a://example.net",
+            "new_value": "b",
+            "expected": {
+                "href": "b://example.net",
+                "protocol": "b:"
+            }
+        },
+        {
+            "href": "javascript:alert(1)",
+            "new_value": "defuse",
+            "expected": {
+                "href": "defuse:alert(1)",
+                "protocol": "defuse:"
+            }
+        },
+        {
+            "comment": "Upper-case ASCII is lower-cased",
+            "href": "a://example.net",
+            "new_value": "B",
+            "expected": {
+                "href": "b://example.net",
+                "protocol": "b:"
+            }
+        },
+        {
+            "comment": "Non-ASCII is rejected",
+            "href": "a://example.net",
+            "new_value": "é",
+            "expected": {
+                "href": "a://example.net",
+                "protocol": "a:"
+            }
+        },
+        {
+            "comment": "No leading digit",
+            "href": "a://example.net",
+            "new_value": "0b",
+            "expected": {
+                "href": "a://example.net",
+                "protocol": "a:"
+            }
+        },
+        {
+            "comment": "No leading punctuation",
+            "href": "a://example.net",
+            "new_value": "+b",
+            "expected": {
+                "href": "a://example.net",
+                "protocol": "a:"
+            }
+        },
+        {
+            "href": "a://example.net",
+            "new_value": "bC0+-.",
+            "expected": {
+                "href": "bc0+-.://example.net",
+                "protocol": "bc0+-.:"
+            }
+        },
+        {
+            "comment": "Only some punctuation is acceptable",
+            "href": "a://example.net",
+            "new_value": "b,c",
+            "expected": {
+                "href": "a://example.net",
+                "protocol": "a:"
+            }
+        },
+        {
+            "comment": "Non-ASCII is rejected",
+            "href": "a://example.net",
+            "new_value": "bé",
+            "expected": {
+                "href": "a://example.net",
+                "protocol": "a:"
+            }
+        },
+        {
+            "comment": "Can’t switch from URL containing username/password/port to file",
+            "href": "http://test@example.net",
+            "new_value": "file",
+            "expected": {
+                "href": "http://test@example.net/",
+                "protocol": "http:"
+            }
+        },
+        {
+            "href": "gopher://example.net:1234",
+            "new_value": "file",
+            "expected": {
+                "href": "gopher://example.net:1234/",
+                "protocol": "gopher:"
+            }
+        },
+        {
+            "href": "wss://x:x@example.net:1234",
+            "new_value": "file",
+            "expected": {
+                "href": "wss://x:x@example.net:1234/",
+                "protocol": "wss:"
+            }
+        },
+        {
+            "comment": "Can’t switch from file URL with no host",
+            "href": "file://localhost/",
+            "new_value": "http",
+            "expected": {
+                "href": "file:///",
+                "protocol": "file:"
+            }
+        },
+        {
+            "href": "file:///test",
+            "new_value": "gopher",
+            "expected": {
+                "href": "file:///test",
+                "protocol": "file:"
+            }
+        },
+        {
+            "href": "file:",
+            "new_value": "wss",
+            "expected": {
+                "href": "file:///",
+                "protocol": "file:"
+            }
+        },
+        {
+            "comment": "Can’t switch from special scheme to non-special",
+            "href": "http://example.net",
+            "new_value": "b",
+            "expected": {
+                "href": "http://example.net/",
+                "protocol": "http:"
+            }
+        },
+        {
+            "href": "file://hi/path",
+            "new_value": "s",
+            "expected": {
+                "href": "file://hi/path",
+                "protocol": "file:"
+            }
+        },
+        {
+            "href": "https://example.net",
+            "new_value": "s",
+            "expected": {
+                "href": "https://example.net/",
+                "protocol": "https:"
+            }
+        },
+        {
+            "href": "ftp://example.net",
+            "new_value": "test",
+            "expected": {
+                "href": "ftp://example.net/",
+                "protocol": "ftp:"
+            }
+        },
+        {
+            "comment": "Cannot-be-a-base URL doesn’t have a host, but URL in a special scheme must.",
+            "href": "mailto:me@example.net",
+            "new_value": "http",
+            "expected": {
+                "href": "mailto:me@example.net",
+                "protocol": "mailto:"
+            }
+        },
+        {
+            "comment": "Can’t switch from non-special scheme to special",
+            "href": "ssh://me@example.net",
+            "new_value": "http",
+            "expected": {
+                "href": "ssh://me@example.net",
+                "protocol": "ssh:"
+            }
+        },
+        {
+            "href": "ssh://me@example.net",
+            "new_value": "gopher",
+            "expected": {
+                "href": "ssh://me@example.net",
+                "protocol": "ssh:"
+            }
+        },
+        {
+            "href": "ssh://me@example.net",
+            "new_value": "file",
+            "expected": {
+                "href": "ssh://me@example.net",
+                "protocol": "ssh:"
+            }
+        },
+        {
+            "href": "ssh://example.net",
+            "new_value": "file",
+            "expected": {
+                "href": "ssh://example.net",
+                "protocol": "ssh:"
+            }
+        },
+        {
+            "href": "nonsense:///test",
+            "new_value": "https",
+            "expected": {
+                "href": "nonsense:///test",
+                "protocol": "nonsense:"
+            }
+        },
+        {
+            "comment": "Stuff after the first ':' is ignored",
+            "href": "http://example.net",
+            "new_value": "https:foo : bar",
+            "expected": {
+                "href": "https://example.net/",
+                "protocol": "https:"
+            }
+        },
+        {
+            "comment": "Stuff after the first ':' is ignored",
+            "href": "data:text/html,<p>Test",
+            "new_value": "view-source+data:foo : bar",
+            "expected": {
+                "href": "view-source+data:text/html,<p>Test",
+                "protocol": "view-source+data:"
+            }
+        },
+        {
+            "comment": "Port is set to null if it is the default for new scheme.",
+            "href": "http://foo.com:443/",
+            "new_value": "https",
+            "expected": {
+                "href": "https://foo.com/",
+                "protocol": "https:",
+                "port": ""
+            }
+        }
+    ],
+    "username": [
+        {
+            "comment": "No host means no username",
+            "href": "file:///home/you/index.html",
+            "new_value": "me",
+            "expected": {
+                "href": "file:///home/you/index.html",
+                "username": ""
+            }
+        },
+        {
+            "comment": "No host means no username",
+            "href": "unix:/run/foo.socket",
+            "new_value": "me",
+            "expected": {
+                "href": "unix:/run/foo.socket",
+                "username": ""
+            }
+        },
+        {
+            "comment": "Cannot-be-a-base means no username",
+            "href": "mailto:you@example.net",
+            "new_value": "me",
+            "expected": {
+                "href": "mailto:you@example.net",
+                "username": ""
+            }
+        },
+        {
+            "href": "javascript:alert(1)",
+            "new_value": "wario",
+            "expected": {
+                "href": "javascript:alert(1)",
+                "username": ""
+            }
+        },
+        {
+            "href": "http://example.net",
+            "new_value": "me",
+            "expected": {
+                "href": "http://me@example.net/",
+                "username": "me"
+            }
+        },
+        {
+            "href": "http://:secret@example.net",
+            "new_value": "me",
+            "expected": {
+                "href": "http://me:secret@example.net/",
+                "username": "me"
+            }
+        },
+        {
+            "href": "http://me@example.net",
+            "new_value": "",
+            "expected": {
+                "href": "http://example.net/",
+                "username": ""
+            }
+        },
+        {
+            "href": "http://me:secret@example.net",
+            "new_value": "",
+            "expected": {
+                "href": "http://:secret@example.net/",
+                "username": ""
+            }
+        },
+        {
+            "comment": "UTF-8 percent encoding with the userinfo encode set.",
+            "href": "http://example.net",
+            "new_value": "\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\u007f\u0080\u0081Éé",
+            "expected": {
+                "href": "http://%00%01%09%0A%0D%1F%20!%22%23$%&'()*+,-.%2F09%3A%3B%3C%3D%3E%3F%40AZ%5B%5C%5D%5E_%60az%7B%7C%7D~%7F%C2%80%C2%81%C3%89%C3%A9@example.net/",
+                "username": "%00%01%09%0A%0D%1F%20!%22%23$%&'()*+,-.%2F09%3A%3B%3C%3D%3E%3F%40AZ%5B%5C%5D%5E_%60az%7B%7C%7D~%7F%C2%80%C2%81%C3%89%C3%A9"
+            }
+        },
+        {
+            "comment": "Bytes already percent-encoded are left as-is.",
+            "href": "http://example.net",
+            "new_value": "%c3%89té",
+            "expected": {
+                "href": "http://%c3%89t%C3%A9@example.net/",
+                "username": "%c3%89t%C3%A9"
+            }
+        },
+        {
+            "href": "sc:///",
+            "new_value": "x",
+            "expected": {
+                "href": "sc:///",
+                "username": ""
+            }
+        },
+        {
+            "href": "javascript://x/",
+            "new_value": "wario",
+            "expected": {
+                "href": "javascript://wario@x/",
+                "username": "wario"
+            }
+        },
+        {
+            "href": "file://test/",
+            "new_value": "test",
+            "expected": {
+                "href": "file://test/",
+                "username": ""
+            }
+        }
+    ],
+    "password": [
+        {
+            "comment": "No host means no password",
+            "href": "file:///home/me/index.html",
+            "new_value": "secret",
+            "expected": {
+                "href": "file:///home/me/index.html",
+                "password": ""
+            }
+        },
+        {
+            "comment": "No host means no password",
+            "href": "unix:/run/foo.socket",
+            "new_value": "secret",
+            "expected": {
+                "href": "unix:/run/foo.socket",
+                "password": ""
+            }
+        },
+        {
+            "comment": "Cannot-be-a-base means no password",
+            "href": "mailto:me@example.net",
+            "new_value": "secret",
+            "expected": {
+                "href": "mailto:me@example.net",
+                "password": ""
+            }
+        },
+        {
+            "href": "http://example.net",
+            "new_value": "secret",
+            "expected": {
+                "href": "http://:secret@example.net/",
+                "password": "secret"
+            }
+        },
+        {
+            "href": "http://me@example.net",
+            "new_value": "secret",
+            "expected": {
+                "href": "http://me:secret@example.net/",
+                "password": "secret"
+            }
+        },
+        {
+            "href": "http://:secret@example.net",
+            "new_value": "",
+            "expected": {
+                "href": "http://example.net/",
+                "password": ""
+            }
+        },
+        {
+            "href": "http://me:secret@example.net",
+            "new_value": "",
+            "expected": {
+                "href": "http://me@example.net/",
+                "password": ""
+            }
+        },
+        {
+            "comment": "UTF-8 percent encoding with the userinfo encode set.",
+            "href": "http://example.net",
+            "new_value": "\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\u007f\u0080\u0081Éé",
+            "expected": {
+                "href": "http://:%00%01%09%0A%0D%1F%20!%22%23$%&'()*+,-.%2F09%3A%3B%3C%3D%3E%3F%40AZ%5B%5C%5D%5E_%60az%7B%7C%7D~%7F%C2%80%C2%81%C3%89%C3%A9@example.net/",
+                "password": "%00%01%09%0A%0D%1F%20!%22%23$%&'()*+,-.%2F09%3A%3B%3C%3D%3E%3F%40AZ%5B%5C%5D%5E_%60az%7B%7C%7D~%7F%C2%80%C2%81%C3%89%C3%A9"
+            }
+        },
+        {
+            "comment": "Bytes already percent-encoded are left as-is.",
+            "href": "http://example.net",
+            "new_value": "%c3%89té",
+            "expected": {
+                "href": "http://:%c3%89t%C3%A9@example.net/",
+                "password": "%c3%89t%C3%A9"
+            }
+        },
+        {
+            "href": "sc:///",
+            "new_value": "x",
+            "expected": {
+                "href": "sc:///",
+                "password": ""
+            }
+        },
+        {
+            "href": "javascript://x/",
+            "new_value": "bowser",
+            "expected": {
+                "href": "javascript://:bowser@x/",
+                "password": "bowser"
+            }
+        },
+        {
+            "href": "file://test/",
+            "new_value": "test",
+            "expected": {
+                "href": "file://test/",
+                "password": ""
+            }
+        }
+    ],
+    "host": [
+        {
+            "comment": "Non-special scheme",
+            "href": "sc://x/",
+            "new_value": "\u0000",
+            "expected": {
+                "href": "sc://x/",
+                "host": "x",
+                "hostname": "x"
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "\u0009",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "\u000A",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "\u000D",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": " ",
+            "expected": {
+                "href": "sc://x/",
+                "host": "x",
+                "hostname": "x"
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "#",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "/",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "?",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "@",
+            "expected": {
+                "href": "sc://x/",
+                "host": "x",
+                "hostname": "x"
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "ß",
+            "expected": {
+                "href": "sc://%C3%9F/",
+                "host": "%C3%9F",
+                "hostname": "%C3%9F"
+            }
+        },
+        {
+            "comment": "IDNA Nontransitional_Processing",
+            "href": "https://x/",
+            "new_value": "ß",
+            "expected": {
+                "href": "https://xn--zca/",
+                "host": "xn--zca",
+                "hostname": "xn--zca"
+            }
+        },
+        {
+            "comment": "Cannot-be-a-base means no host",
+            "href": "mailto:me@example.net",
+            "new_value": "example.com",
+            "expected": {
+                "href": "mailto:me@example.net",
+                "host": ""
+            }
+        },
+        {
+            "comment": "Cannot-be-a-base means no password",
+            "href": "data:text/plain,Stuff",
+            "new_value": "example.net",
+            "expected": {
+                "href": "data:text/plain,Stuff",
+                "host": ""
+            }
+        },
+        {
+            "href": "http://example.net",
+            "new_value": "example.com:8080",
+            "expected": {
+                "href": "http://example.com:8080/",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Port number is unchanged if not specified in the new value",
+            "href": "http://example.net:8080",
+            "new_value": "example.com",
+            "expected": {
+                "href": "http://example.com:8080/",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Port number is unchanged if not specified",
+            "href": "http://example.net:8080",
+            "new_value": "example.com:",
+            "expected": {
+                "href": "http://example.com:8080/",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "The empty host is not valid for special schemes",
+            "href": "http://example.net",
+            "new_value": "",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net"
+            }
+        },
+        {
+            "comment": "The empty host is OK for non-special schemes",
+            "href": "view-source+http://example.net/foo",
+            "new_value": "",
+            "expected": {
+                "href": "view-source+http:///foo",
+                "host": ""
+            }
+        },
+        {
+            "comment": "Path-only URLs can gain a host",
+            "href": "a:/foo",
+            "new_value": "example.net",
+            "expected": {
+                "href": "a://example.net/foo",
+                "host": "example.net"
+            }
+        },
+        {
+            "comment": "IPv4 address syntax is normalized",
+            "href": "http://example.net",
+            "new_value": "0x7F000001:8080",
+            "expected": {
+                "href": "http://127.0.0.1:8080/",
+                "host": "127.0.0.1:8080",
+                "hostname": "127.0.0.1",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "IPv6 address syntax is normalized",
+            "href": "http://example.net",
+            "new_value": "[::0:01]:2",
+            "expected": {
+                "href": "http://[::1]:2/",
+                "host": "[::1]:2",
+                "hostname": "[::1]",
+                "port": "2"
+            }
+        },
+        {
+            "comment": "Default port number is removed",
+            "href": "http://example.net",
+            "new_value": "example.com:80",
+            "expected": {
+                "href": "http://example.com/",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Default port number is removed",
+            "href": "https://example.net",
+            "new_value": "example.com:443",
+            "expected": {
+                "href": "https://example.com/",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Default port number is only removed for the relevant scheme",
+            "href": "https://example.net",
+            "new_value": "example.com:80",
+            "expected": {
+                "href": "https://example.com:80/",
+                "host": "example.com:80",
+                "hostname": "example.com",
+                "port": "80"
+            }
+        },
+        {
+            "comment": "Stuff after a / delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "example.com/stuff",
+            "expected": {
+                "href": "http://example.com/path",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Stuff after a / delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "example.com:8080/stuff",
+            "expected": {
+                "href": "http://example.com:8080/path",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Stuff after a ? delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "example.com?stuff",
+            "expected": {
+                "href": "http://example.com/path",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Stuff after a ? delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "example.com:8080?stuff",
+            "expected": {
+                "href": "http://example.com:8080/path",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Stuff after a # delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "example.com#stuff",
+            "expected": {
+                "href": "http://example.com/path",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Stuff after a # delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "example.com:8080#stuff",
+            "expected": {
+                "href": "http://example.com:8080/path",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Stuff after a \\ delimiter is ignored for special schemes",
+            "href": "http://example.net/path",
+            "new_value": "example.com\\stuff",
+            "expected": {
+                "href": "http://example.com/path",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Stuff after a \\ delimiter is ignored for special schemes",
+            "href": "http://example.net/path",
+            "new_value": "example.com:8080\\stuff",
+            "expected": {
+                "href": "http://example.com:8080/path",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "\\ is not a delimiter for non-special schemes, but still forbidden in hosts",
+            "href": "view-source+http://example.net/path",
+            "new_value": "example.com\\stuff",
+            "expected": {
+                "href": "view-source+http://example.net/path",
+                "host": "example.net",
+                "hostname": "example.net",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error",
+            "href": "view-source+http://example.net/path",
+            "new_value": "example.com:8080stuff2",
+            "expected": {
+                "href": "view-source+http://example.com:8080/path",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error",
+            "href": "http://example.net/path",
+            "new_value": "example.com:8080stuff2",
+            "expected": {
+                "href": "http://example.com:8080/path",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error",
+            "href": "http://example.net/path",
+            "new_value": "example.com:8080+2",
+            "expected": {
+                "href": "http://example.com:8080/path",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Port numbers are 16 bit integers",
+            "href": "http://example.net/path",
+            "new_value": "example.com:65535",
+            "expected": {
+                "href": "http://example.com:65535/path",
+                "host": "example.com:65535",
+                "hostname": "example.com",
+                "port": "65535"
+            }
+        },
+        {
+            "comment": "Port numbers are 16 bit integers, overflowing is an error. Hostname is still set, though.",
+            "href": "http://example.net/path",
+            "new_value": "example.com:65536",
+            "expected": {
+                "href": "http://example.com/path",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Broken IPv6",
+            "href": "http://example.net/",
+            "new_value": "[google.com]",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net"
+            }
+        },
+        {
+            "href": "http://example.net/",
+            "new_value": "[::1.2.3.4x]",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net"
+            }
+        },
+        {
+            "href": "http://example.net/",
+            "new_value": "[::1.2.3.]",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net"
+            }
+        },
+        {
+            "href": "http://example.net/",
+            "new_value": "[::1.2.]",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net"
+            }
+        },
+        {
+            "href": "http://example.net/",
+            "new_value": "[::1.]",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net"
+            }
+        },
+        {
+            "href": "file://y/",
+            "new_value": "x:123",
+            "expected": {
+                "href": "file://y/",
+                "host": "y",
+                "hostname": "y",
+                "port": ""
+            }
+        },
+        {
+            "href": "file://y/",
+            "new_value": "loc%41lhost",
+            "expected": {
+                "href": "file:///",
+                "host": "",
+                "hostname": "",
+                "port": ""
+            }
+        },
+        {
+            "href": "file://hi/x",
+            "new_value": "",
+            "expected": {
+                "href": "file:///x",
+                "host": "",
+                "hostname": "",
+                "port": ""
+            }
+        },
+        {
+            "href": "sc://test@test/",
+            "new_value": "",
+            "expected": {
+                "href": "sc://test@test/",
+                "host": "test",
+                "hostname": "test",
+                "username": "test"
+            }
+        },
+        {
+            "href": "sc://test:12/",
+            "new_value": "",
+            "expected": {
+                "href": "sc://test:12/",
+                "host": "test:12",
+                "hostname": "test",
+                "port": "12"
+            }
+        }
+    ],
+    "hostname": [
+        {
+            "comment": "Non-special scheme",
+            "href": "sc://x/",
+            "new_value": "\u0000",
+            "expected": {
+                "href": "sc://x/",
+                "host": "x",
+                "hostname": "x"
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "\u0009",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "\u000A",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "\u000D",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": " ",
+            "expected": {
+                "href": "sc://x/",
+                "host": "x",
+                "hostname": "x"
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "#",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "/",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "?",
+            "expected": {
+                "href": "sc:///",
+                "host": "",
+                "hostname": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "@",
+            "expected": {
+                "href": "sc://x/",
+                "host": "x",
+                "hostname": "x"
+            }
+        },
+        {
+            "comment": "Cannot-be-a-base means no host",
+            "href": "mailto:me@example.net",
+            "new_value": "example.com",
+            "expected": {
+                "href": "mailto:me@example.net",
+                "host": ""
+            }
+        },
+        {
+            "comment": "Cannot-be-a-base means no password",
+            "href": "data:text/plain,Stuff",
+            "new_value": "example.net",
+            "expected": {
+                "href": "data:text/plain,Stuff",
+                "host": ""
+            }
+        },
+        {
+            "href": "http://example.net:8080",
+            "new_value": "example.com",
+            "expected": {
+                "href": "http://example.com:8080/",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "The empty host is not valid for special schemes",
+            "href": "http://example.net",
+            "new_value": "",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net"
+            }
+        },
+        {
+            "comment": "The empty host is OK for non-special schemes",
+            "href": "view-source+http://example.net/foo",
+            "new_value": "",
+            "expected": {
+                "href": "view-source+http:///foo",
+                "host": ""
+            }
+        },
+        {
+            "comment": "Path-only URLs can gain a host",
+            "href": "a:/foo",
+            "new_value": "example.net",
+            "expected": {
+                "href": "a://example.net/foo",
+                "host": "example.net"
+            }
+        },
+        {
+            "comment": "IPv4 address syntax is normalized",
+            "href": "http://example.net:8080",
+            "new_value": "0x7F000001",
+            "expected": {
+                "href": "http://127.0.0.1:8080/",
+                "host": "127.0.0.1:8080",
+                "hostname": "127.0.0.1",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "IPv6 address syntax is normalized",
+            "href": "http://example.net",
+            "new_value": "[::0:01]",
+            "expected": {
+                "href": "http://[::1]/",
+                "host": "[::1]",
+                "hostname": "[::1]",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Stuff after a : delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "example.com:8080",
+            "expected": {
+                "href": "http://example.com/path",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Stuff after a : delimiter is ignored",
+            "href": "http://example.net:8080/path",
+            "new_value": "example.com:",
+            "expected": {
+                "href": "http://example.com:8080/path",
+                "host": "example.com:8080",
+                "hostname": "example.com",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Stuff after a / delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "example.com/stuff",
+            "expected": {
+                "href": "http://example.com/path",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Stuff after a ? delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "example.com?stuff",
+            "expected": {
+                "href": "http://example.com/path",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Stuff after a # delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "example.com#stuff",
+            "expected": {
+                "href": "http://example.com/path",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Stuff after a \\ delimiter is ignored for special schemes",
+            "href": "http://example.net/path",
+            "new_value": "example.com\\stuff",
+            "expected": {
+                "href": "http://example.com/path",
+                "host": "example.com",
+                "hostname": "example.com",
+                "port": ""
+            }
+        },
+        {
+            "comment": "\\ is not a delimiter for non-special schemes, but still forbidden in hosts",
+            "href": "view-source+http://example.net/path",
+            "new_value": "example.com\\stuff",
+            "expected": {
+                "href": "view-source+http://example.net/path",
+                "host": "example.net",
+                "hostname": "example.net",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Broken IPv6",
+            "href": "http://example.net/",
+            "new_value": "[google.com]",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net"
+            }
+        },
+        {
+            "href": "http://example.net/",
+            "new_value": "[::1.2.3.4x]",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net"
+            }
+        },
+        {
+            "href": "http://example.net/",
+            "new_value": "[::1.2.3.]",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net"
+            }
+        },
+        {
+            "href": "http://example.net/",
+            "new_value": "[::1.2.]",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net"
+            }
+        },
+        {
+            "href": "http://example.net/",
+            "new_value": "[::1.]",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net"
+            }
+        },
+        {
+            "href": "file://y/",
+            "new_value": "x:123",
+            "expected": {
+                "href": "file://y/",
+                "host": "y",
+                "hostname": "y",
+                "port": ""
+            }
+        },
+        {
+            "href": "file://y/",
+            "new_value": "loc%41lhost",
+            "expected": {
+                "href": "file:///",
+                "host": "",
+                "hostname": "",
+                "port": ""
+            }
+        },
+        {
+            "href": "file://hi/x",
+            "new_value": "",
+            "expected": {
+                "href": "file:///x",
+                "host": "",
+                "hostname": "",
+                "port": ""
+            }
+        },
+        {
+            "href": "sc://test@test/",
+            "new_value": "",
+            "expected": {
+                "href": "sc://test@test/",
+                "host": "test",
+                "hostname": "test",
+                "username": "test"
+            }
+        },
+        {
+            "href": "sc://test:12/",
+            "new_value": "",
+            "expected": {
+                "href": "sc://test:12/",
+                "host": "test:12",
+                "hostname": "test",
+                "port": "12"
+            }
+        }
+    ],
+    "port": [
+        {
+            "href": "http://example.net",
+            "new_value": "8080",
+            "expected": {
+                "href": "http://example.net:8080/",
+                "host": "example.net:8080",
+                "hostname": "example.net",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Port number is removed if empty is the new value",
+            "href": "http://example.net:8080",
+            "new_value": "",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Default port number is removed",
+            "href": "http://example.net:8080",
+            "new_value": "80",
+            "expected": {
+                "href": "http://example.net/",
+                "host": "example.net",
+                "hostname": "example.net",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Default port number is removed",
+            "href": "https://example.net:4433",
+            "new_value": "443",
+            "expected": {
+                "href": "https://example.net/",
+                "host": "example.net",
+                "hostname": "example.net",
+                "port": ""
+            }
+        },
+        {
+            "comment": "Default port number is only removed for the relevant scheme",
+            "href": "https://example.net",
+            "new_value": "80",
+            "expected": {
+                "href": "https://example.net:80/",
+                "host": "example.net:80",
+                "hostname": "example.net",
+                "port": "80"
+            }
+        },
+        {
+            "comment": "Stuff after a / delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "8080/stuff",
+            "expected": {
+                "href": "http://example.net:8080/path",
+                "host": "example.net:8080",
+                "hostname": "example.net",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Stuff after a ? delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "8080?stuff",
+            "expected": {
+                "href": "http://example.net:8080/path",
+                "host": "example.net:8080",
+                "hostname": "example.net",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Stuff after a # delimiter is ignored",
+            "href": "http://example.net/path",
+            "new_value": "8080#stuff",
+            "expected": {
+                "href": "http://example.net:8080/path",
+                "host": "example.net:8080",
+                "hostname": "example.net",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Stuff after a \\ delimiter is ignored for special schemes",
+            "href": "http://example.net/path",
+            "new_value": "8080\\stuff",
+            "expected": {
+                "href": "http://example.net:8080/path",
+                "host": "example.net:8080",
+                "hostname": "example.net",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error",
+            "href": "view-source+http://example.net/path",
+            "new_value": "8080stuff2",
+            "expected": {
+                "href": "view-source+http://example.net:8080/path",
+                "host": "example.net:8080",
+                "hostname": "example.net",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error",
+            "href": "http://example.net/path",
+            "new_value": "8080stuff2",
+            "expected": {
+                "href": "http://example.net:8080/path",
+                "host": "example.net:8080",
+                "hostname": "example.net",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error",
+            "href": "http://example.net/path",
+            "new_value": "8080+2",
+            "expected": {
+                "href": "http://example.net:8080/path",
+                "host": "example.net:8080",
+                "hostname": "example.net",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Port numbers are 16 bit integers",
+            "href": "http://example.net/path",
+            "new_value": "65535",
+            "expected": {
+                "href": "http://example.net:65535/path",
+                "host": "example.net:65535",
+                "hostname": "example.net",
+                "port": "65535"
+            }
+        },
+        {
+            "comment": "Port numbers are 16 bit integers, overflowing is an error",
+            "href": "http://example.net:8080/path",
+            "new_value": "65536",
+            "expected": {
+                "href": "http://example.net:8080/path",
+                "host": "example.net:8080",
+                "hostname": "example.net",
+                "port": "8080"
+            }
+        },
+        {
+            "comment": "Port numbers are 16 bit integers, overflowing is an error",
+            "href": "non-special://example.net:8080/path",
+            "new_value": "65536",
+            "expected": {
+                "href": "non-special://example.net:8080/path",
+                "host": "example.net:8080",
+                "hostname": "example.net",
+                "port": "8080"
+            }
+        },
+        {
+            "href": "file://test/",
+            "new_value": "12",
+            "expected": {
+                "href": "file://test/",
+                "port": ""
+            }
+        },
+        {
+            "href": "file://localhost/",
+            "new_value": "12",
+            "expected": {
+                "href": "file:///",
+                "port": ""
+            }
+        },
+        {
+            "href": "non-base:value",
+            "new_value": "12",
+            "expected": {
+                "href": "non-base:value",
+                "port": ""
+            }
+        },
+        {
+            "href": "sc:///",
+            "new_value": "12",
+            "expected": {
+                "href": "sc:///",
+                "port": ""
+            }
+        },
+        {
+            "href": "sc://x/",
+            "new_value": "12",
+            "expected": {
+                "href": "sc://x:12/",
+                "port": "12"
+            }
+        },
+        {
+            "href": "javascript://x/",
+            "new_value": "12",
+            "expected": {
+                "href": "javascript://x:12/",
+                "port": "12"
+            }
+        }
+    ],
+    "pathname": [
+        {
+            "comment": "Cannot-be-a-base don’t have a path",
+            "href": "mailto:me@example.net",
+            "new_value": "/foo",
+            "expected": {
+                "href": "mailto:me@example.net",
+                "pathname": "me@example.net"
+            }
+        },
+        {
+            "href": "unix:/run/foo.socket?timeout=10",
+            "new_value": "/var/log/../run/bar.socket",
+            "expected": {
+                "href": "unix:/var/run/bar.socket?timeout=10",
+                "pathname": "/var/run/bar.socket"
+            }
+        },
+        {
+            "href": "https://example.net#nav",
+            "new_value": "home",
+            "expected": {
+                "href": "https://example.net/home#nav",
+                "pathname": "/home"
+            }
+        },
+        {
+            "href": "https://example.net#nav",
+            "new_value": "../home",
+            "expected": {
+                "href": "https://example.net/home#nav",
+                "pathname": "/home"
+            }
+        },
+        {
+            "comment": "\\ is a segment delimiter for 'special' URLs",
+            "href": "http://example.net/home?lang=fr#nav",
+            "new_value": "\\a\\%2E\\b\\%2e.\\c",
+            "expected": {
+                "href": "http://example.net/a/c?lang=fr#nav",
+                "pathname": "/a/c"
+            }
+        },
+        {
+            "comment": "\\ is *not* a segment delimiter for non-'special' URLs",
+            "href": "view-source+http://example.net/home?lang=fr#nav",
+            "new_value": "\\a\\%2E\\b\\%2e.\\c",
+            "expected": {
+                "href": "view-source+http://example.net/\\a\\%2E\\b\\%2e.\\c?lang=fr#nav",
+                "pathname": "/\\a\\%2E\\b\\%2e.\\c"
+            }
+        },
+        {
+            "comment": "UTF-8 percent encoding with the default encode set. Tabs and newlines are removed.",
+            "href": "a:/",
+            "new_value": "\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\u007f\u0080\u0081Éé",
+            "expected": {
+                "href": "a:/%00%01%1F%20!%22%23$%&'()*+,-./09:;%3C=%3E%3F@AZ[\\]^_%60az%7B|%7D~%7F%C2%80%C2%81%C3%89%C3%A9",
+                "pathname": "/%00%01%1F%20!%22%23$%&'()*+,-./09:;%3C=%3E%3F@AZ[\\]^_%60az%7B|%7D~%7F%C2%80%C2%81%C3%89%C3%A9"
+            }
+        },
+        {
+            "comment": "Bytes already percent-encoded are left as-is, including %2E outside dotted segments.",
+            "href": "http://example.net",
+            "new_value": "%2e%2E%c3%89té",
+            "expected": {
+                "href": "http://example.net/%2e%2E%c3%89t%C3%A9",
+                "pathname": "/%2e%2E%c3%89t%C3%A9"
+            }
+        },
+        {
+            "comment": "? needs to be encoded",
+            "href": "http://example.net",
+            "new_value": "?",
+            "expected": {
+                "href": "http://example.net/%3F",
+                "pathname": "/%3F"
+            }
+        },
+        {
+            "comment": "# needs to be encoded",
+            "href": "http://example.net",
+            "new_value": "#",
+            "expected": {
+                "href": "http://example.net/%23",
+                "pathname": "/%23"
+            }
+        },
+        {
+            "comment": "? needs to be encoded, non-special scheme",
+            "href": "sc://example.net",
+            "new_value": "?",
+            "expected": {
+                "href": "sc://example.net/%3F",
+                "pathname": "/%3F"
+            }
+        },
+        {
+            "comment": "# needs to be encoded, non-special scheme",
+            "href": "sc://example.net",
+            "new_value": "#",
+            "expected": {
+                "href": "sc://example.net/%23",
+                "pathname": "/%23"
+            }
+        },
+        {
+            "comment": "File URLs and (back)slashes",
+            "href": "file://monkey/",
+            "new_value": "\\\\",
+            "expected": {
+                "href": "file://monkey/",
+                "pathname": "/"
+            }
+        },
+        {
+            "comment": "File URLs and (back)slashes",
+            "href": "file:///unicorn",
+            "new_value": "//\\/",
+            "expected": {
+                "href": "file:///",
+                "pathname": "/"
+            }
+        },
+        {
+            "comment": "File URLs and (back)slashes",
+            "href": "file:///unicorn",
+            "new_value": "//monkey/..//",
+            "expected": {
+                "href": "file:///",
+                "pathname": "/"
+            }
+        }
+    ],
+    "search": [
+        {
+            "href": "https://example.net#nav",
+            "new_value": "lang=fr",
+            "expected": {
+                "href": "https://example.net/?lang=fr#nav",
+                "search": "?lang=fr"
+            }
+        },
+        {
+            "href": "https://example.net?lang=en-US#nav",
+            "new_value": "lang=fr",
+            "expected": {
+                "href": "https://example.net/?lang=fr#nav",
+                "search": "?lang=fr"
+            }
+        },
+        {
+            "href": "https://example.net?lang=en-US#nav",
+            "new_value": "?lang=fr",
+            "expected": {
+                "href": "https://example.net/?lang=fr#nav",
+                "search": "?lang=fr"
+            }
+        },
+        {
+            "href": "https://example.net?lang=en-US#nav",
+            "new_value": "??lang=fr",
+            "expected": {
+                "href": "https://example.net/??lang=fr#nav",
+                "search": "??lang=fr"
+            }
+        },
+        {
+            "href": "https://example.net?lang=en-US#nav",
+            "new_value": "?",
+            "expected": {
+                "href": "https://example.net/?#nav",
+                "search": ""
+            }
+        },
+        {
+            "href": "https://example.net?lang=en-US#nav",
+            "new_value": "",
+            "expected": {
+                "href": "https://example.net/#nav",
+                "search": ""
+            }
+        },
+        {
+            "href": "https://example.net?lang=en-US",
+            "new_value": "",
+            "expected": {
+                "href": "https://example.net/",
+                "search": ""
+            }
+        },
+        {
+            "href": "https://example.net",
+            "new_value": "",
+            "expected": {
+                "href": "https://example.net/",
+                "search": ""
+            }
+        },
+        {
+            "comment": "UTF-8 percent encoding with the query encode set. Tabs and newlines are removed.",
+            "href": "a:/",
+            "new_value": "\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\u007f\u0080\u0081Éé",
+            "expected": {
+                "href": "a:/?%00%01%1F%20!%22%23$%&'()*+,-./09:;%3C=%3E?@AZ[\\]^_`az{|}~%7F%C2%80%C2%81%C3%89%C3%A9",
+                "search": "?%00%01%1F%20!%22%23$%&'()*+,-./09:;%3C=%3E?@AZ[\\]^_`az{|}~%7F%C2%80%C2%81%C3%89%C3%A9"
+            }
+        },
+        {
+            "comment": "Bytes already percent-encoded are left as-is",
+            "href": "http://example.net",
+            "new_value": "%c3%89té",
+            "expected": {
+                "href": "http://example.net/?%c3%89t%C3%A9",
+                "search": "?%c3%89t%C3%A9"
+            }
+        }
+    ],
+    "hash": [
+        {
+            "href": "https://example.net",
+            "new_value": "main",
+            "expected": {
+                "href": "https://example.net/#main",
+                "hash": "#main"
+            }
+        },
+        {
+            "href": "https://example.net#nav",
+            "new_value": "main",
+            "expected": {
+                "href": "https://example.net/#main",
+                "hash": "#main"
+            }
+        },
+        {
+            "href": "https://example.net?lang=en-US",
+            "new_value": "##nav",
+            "expected": {
+                "href": "https://example.net/?lang=en-US##nav",
+                "hash": "##nav"
+            }
+        },
+        {
+            "href": "https://example.net?lang=en-US#nav",
+            "new_value": "#main",
+            "expected": {
+                "href": "https://example.net/?lang=en-US#main",
+                "hash": "#main"
+            }
+        },
+        {
+            "href": "https://example.net?lang=en-US#nav",
+            "new_value": "#",
+            "expected": {
+                "href": "https://example.net/?lang=en-US#",
+                "hash": ""
+            }
+        },
+        {
+            "href": "https://example.net?lang=en-US#nav",
+            "new_value": "",
+            "expected": {
+                "href": "https://example.net/?lang=en-US",
+                "hash": ""
+            }
+        },
+        {
+            "comment": "Simple percent-encoding; nuls, tabs, and newlines are removed",
+            "href": "a:/",
+            "new_value": "\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\u007f\u0080\u0081Éé",
+            "expected": {
+                "href": "a:/#%01%1F !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~%7F%C2%80%C2%81%C3%89%C3%A9",
+                "hash": "#%01%1F !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~%7F%C2%80%C2%81%C3%89%C3%A9"
+            }
+        },
+        {
+            "comment": "Bytes already percent-encoded are left as-is",
+            "href": "http://example.net",
+            "new_value": "%c3%89té",
+            "expected": {
+                "href": "http://example.net/#%c3%89t%C3%A9",
+                "hash": "#%c3%89t%C3%A9"
+            }
+        },
+        {
+            "href": "javascript:alert(1)",
+            "new_value": "castle",
+            "expected": {
+                "href": "javascript:alert(1)#castle",
+                "hash": "#castle"
+            }
+        }
+    ]
+}
diff --git a/test/fixtures/url-tests-additional.js b/test/fixtures/url-tests-additional.js
new file mode 100644
index 00000000000000..c1c640f4bb4b7d
--- /dev/null
+++ b/test/fixtures/url-tests-additional.js
@@ -0,0 +1,36 @@
+'use strict';
+
+// This file contains test cases not part of the WPT
+
+module.exports = [
+  {
+    // surrogate pair
+    'url': 'https://github.com/nodejs/\uD83D\uDE00node',
+    'protocol': 'https:',
+    'pathname': '/nodejs/%F0%9F%98%80node'
+  },
+  {
+    // unpaired low surrogate
+    'url': 'https://github.com/nodejs/\uD83D',
+    'protocol': 'https:',
+    'pathname': '/nodejs/%EF%BF%BD'
+  },
+  {
+    // unpaired low surrogate
+    'url': 'https://github.com/nodejs/\uD83Dnode',
+    'protocol': 'https:',
+    'pathname': '/nodejs/%EF%BF%BDnode'
+  },
+  {
+    // unmatched high surrogate
+    'url': 'https://github.com/nodejs/\uDE00',
+    'protocol': 'https:',
+    'pathname': '/nodejs/%EF%BF%BD'
+  },
+  {
+    // unmatched high surrogate
+    'url': 'https://github.com/nodejs/\uDE00node',
+    'protocol': 'https:',
+    'pathname': '/nodejs/%EF%BF%BDnode'
+  }
+];
diff --git a/test/fixtures/url-tests.js b/test/fixtures/url-tests.js
new file mode 100644
index 00000000000000..48f77fe0774d64
--- /dev/null
+++ b/test/fixtures/url-tests.js
@@ -0,0 +1,6565 @@
+'use strict';
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/11757f1/url/urltestdata.json
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+module.exports =
+[
+  "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/segments.js",
+  {
+    "input": "http://example\t.\norg",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://user:pass@foo:21/bar;par?b#c",
+    "base": "http://example.org/foo/bar",
+    "href": "http://user:pass@foo:21/bar;par?b#c",
+    "origin": "http://foo:21",
+    "protocol": "http:",
+    "username": "user",
+    "password": "pass",
+    "host": "foo:21",
+    "hostname": "foo",
+    "port": "21",
+    "pathname": "/bar;par",
+    "search": "?b",
+    "hash": "#c"
+  },
+  {
+    "input": "https://test:@test",
+    "base": "about:blank",
+    "href": "https://test@test/",
+    "origin": "https://test",
+    "protocol": "https:",
+    "username": "test",
+    "password": "",
+    "host": "test",
+    "hostname": "test",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "https://:@test",
+    "base": "about:blank",
+    "href": "https://test/",
+    "origin": "https://test",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "test",
+    "hostname": "test",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "non-special://test:@test/x",
+    "base": "about:blank",
+    "href": "non-special://test@test/x",
+    "origin": "null",
+    "protocol": "non-special:",
+    "username": "test",
+    "password": "",
+    "host": "test",
+    "hostname": "test",
+    "port": "",
+    "pathname": "/x",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "non-special://:@test/x",
+    "base": "about:blank",
+    "href": "non-special://test/x",
+    "origin": "null",
+    "protocol": "non-special:",
+    "username": "",
+    "password": "",
+    "host": "test",
+    "hostname": "test",
+    "port": "",
+    "pathname": "/x",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:foo.com",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/foo.com",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/foo.com",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "\t   :foo.com   \n",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/:foo.com",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/:foo.com",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": " foo.com  ",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/foo.com",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/foo.com",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "a:\t foo.com",
+    "base": "http://example.org/foo/bar",
+    "href": "a: foo.com",
+    "origin": "null",
+    "protocol": "a:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": " foo.com",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://f:21/ b ? d # e ",
+    "base": "http://example.org/foo/bar",
+    "href": "http://f:21/%20b%20?%20d%20# e",
+    "origin": "http://f:21",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "f:21",
+    "hostname": "f",
+    "port": "21",
+    "pathname": "/%20b%20",
+    "search": "?%20d%20",
+    "hash": "# e"
+  },
+  {
+    "input": "lolscheme:x x#x x",
+    "base": "about:blank",
+    "href": "lolscheme:x x#x x",
+    "protocol": "lolscheme:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "x x",
+    "search": "",
+    "hash": "#x x"
+  },
+  {
+    "input": "http://f:/c",
+    "base": "http://example.org/foo/bar",
+    "href": "http://f/c",
+    "origin": "http://f",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "f",
+    "hostname": "f",
+    "port": "",
+    "pathname": "/c",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://f:0/c",
+    "base": "http://example.org/foo/bar",
+    "href": "http://f:0/c",
+    "origin": "http://f:0",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "f:0",
+    "hostname": "f",
+    "port": "0",
+    "pathname": "/c",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://f:00000000000000/c",
+    "base": "http://example.org/foo/bar",
+    "href": "http://f:0/c",
+    "origin": "http://f:0",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "f:0",
+    "hostname": "f",
+    "port": "0",
+    "pathname": "/c",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://f:00000000000000000000080/c",
+    "base": "http://example.org/foo/bar",
+    "href": "http://f/c",
+    "origin": "http://f",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "f",
+    "hostname": "f",
+    "port": "",
+    "pathname": "/c",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://f:b/c",
+    "base": "http://example.org/foo/bar",
+    "failure": true
+  },
+  {
+    "input": "http://f: /c",
+    "base": "http://example.org/foo/bar",
+    "failure": true
+  },
+  {
+    "input": "http://f:\n/c",
+    "base": "http://example.org/foo/bar",
+    "href": "http://f/c",
+    "origin": "http://f",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "f",
+    "hostname": "f",
+    "port": "",
+    "pathname": "/c",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://f:fifty-two/c",
+    "base": "http://example.org/foo/bar",
+    "failure": true
+  },
+  {
+    "input": "http://f:999999/c",
+    "base": "http://example.org/foo/bar",
+    "failure": true
+  },
+  {
+    "input": "non-special://f:999999/c",
+    "base": "http://example.org/foo/bar",
+    "failure": true
+  },
+  {
+    "input": "http://f: 21 / b ? d # e ",
+    "base": "http://example.org/foo/bar",
+    "failure": true
+  },
+  {
+    "input": "",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/bar",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "  \t",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/bar",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": ":foo.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/:foo.com/",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/:foo.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": ":foo.com\\",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/:foo.com/",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/:foo.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": ":",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/:",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/:",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": ":a",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/:a",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/:a",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": ":/",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/:/",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/:/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": ":\\",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/:/",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/:/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": ":#",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/:#",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/:",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "#",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/bar#",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "#/",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/bar#/",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "",
+    "hash": "#/"
+  },
+  {
+    "input": "#\\",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/bar#\\",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "",
+    "hash": "#\\"
+  },
+  {
+    "input": "#;?",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/bar#;?",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "",
+    "hash": "#;?"
+  },
+  {
+    "input": "?",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/bar?",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": ":23",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/:23",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/:23",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/:23",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/:23",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/:23",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "::",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/::",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/::",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "::23",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/::23",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/::23",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "foo://",
+    "base": "http://example.org/foo/bar",
+    "href": "foo://",
+    "origin": "null",
+    "protocol": "foo:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://a:b@c:29/d",
+    "base": "http://example.org/foo/bar",
+    "href": "http://a:b@c:29/d",
+    "origin": "http://c:29",
+    "protocol": "http:",
+    "username": "a",
+    "password": "b",
+    "host": "c:29",
+    "hostname": "c",
+    "port": "29",
+    "pathname": "/d",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http::@c:29",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/:@c:29",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/:@c:29",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://&a:foo(b]c@d:2/",
+    "base": "http://example.org/foo/bar",
+    "href": "http://&a:foo(b%5Dc@d:2/",
+    "origin": "http://d:2",
+    "protocol": "http:",
+    "username": "&a",
+    "password": "foo(b%5Dc",
+    "host": "d:2",
+    "hostname": "d",
+    "port": "2",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://::@c@d:2",
+    "base": "http://example.org/foo/bar",
+    "href": "http://:%3A%40c@d:2/",
+    "origin": "http://d:2",
+    "protocol": "http:",
+    "username": "",
+    "password": "%3A%40c",
+    "host": "d:2",
+    "hostname": "d",
+    "port": "2",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://foo.com:b@d/",
+    "base": "http://example.org/foo/bar",
+    "href": "http://foo.com:b@d/",
+    "origin": "http://d",
+    "protocol": "http:",
+    "username": "foo.com",
+    "password": "b",
+    "host": "d",
+    "hostname": "d",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://foo.com/\\@",
+    "base": "http://example.org/foo/bar",
+    "href": "http://foo.com//@",
+    "origin": "http://foo.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "foo.com",
+    "hostname": "foo.com",
+    "port": "",
+    "pathname": "//@",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:\\\\foo.com\\",
+    "base": "http://example.org/foo/bar",
+    "href": "http://foo.com/",
+    "origin": "http://foo.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "foo.com",
+    "hostname": "foo.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:\\\\a\\b:c\\d@foo.com\\",
+    "base": "http://example.org/foo/bar",
+    "href": "http://a/b:c/d@foo.com/",
+    "origin": "http://a",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "a",
+    "hostname": "a",
+    "port": "",
+    "pathname": "/b:c/d@foo.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "foo:/",
+    "base": "http://example.org/foo/bar",
+    "href": "foo:/",
+    "origin": "null",
+    "protocol": "foo:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "foo:/bar.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "foo:/bar.com/",
+    "origin": "null",
+    "protocol": "foo:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/bar.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "foo://///////",
+    "base": "http://example.org/foo/bar",
+    "href": "foo://///////",
+    "origin": "null",
+    "protocol": "foo:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "///////",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "foo://///////bar.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "foo://///////bar.com/",
+    "origin": "null",
+    "protocol": "foo:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "///////bar.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "foo:////://///",
+    "base": "http://example.org/foo/bar",
+    "href": "foo:////://///",
+    "origin": "null",
+    "protocol": "foo:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "//://///",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "c:/foo",
+    "base": "http://example.org/foo/bar",
+    "href": "c:/foo",
+    "origin": "null",
+    "protocol": "c:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/foo",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "//foo/bar",
+    "base": "http://example.org/foo/bar",
+    "href": "http://foo/bar",
+    "origin": "http://foo",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "foo",
+    "hostname": "foo",
+    "port": "",
+    "pathname": "/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://foo/path;a??e#f#g",
+    "base": "http://example.org/foo/bar",
+    "href": "http://foo/path;a??e#f#g",
+    "origin": "http://foo",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "foo",
+    "hostname": "foo",
+    "port": "",
+    "pathname": "/path;a",
+    "search": "??e",
+    "hash": "#f#g"
+  },
+  {
+    "input": "http://foo/abcd?efgh?ijkl",
+    "base": "http://example.org/foo/bar",
+    "href": "http://foo/abcd?efgh?ijkl",
+    "origin": "http://foo",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "foo",
+    "hostname": "foo",
+    "port": "",
+    "pathname": "/abcd",
+    "search": "?efgh?ijkl",
+    "hash": ""
+  },
+  {
+    "input": "http://foo/abcd#foo?bar",
+    "base": "http://example.org/foo/bar",
+    "href": "http://foo/abcd#foo?bar",
+    "origin": "http://foo",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "foo",
+    "hostname": "foo",
+    "port": "",
+    "pathname": "/abcd",
+    "search": "",
+    "hash": "#foo?bar"
+  },
+  {
+    "input": "[61:24:74]:98",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/[61:24:74]:98",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/[61:24:74]:98",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:[61:27]/:foo",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/[61:27]/:foo",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/[61:27]/:foo",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://[1::2]:3:4",
+    "base": "http://example.org/foo/bar",
+    "failure": true
+  },
+  {
+    "input": "http://2001::1",
+    "base": "http://example.org/foo/bar",
+    "failure": true
+  },
+  {
+    "input": "http://2001::1]",
+    "base": "http://example.org/foo/bar",
+    "failure": true
+  },
+  {
+    "input": "http://2001::1]:80",
+    "base": "http://example.org/foo/bar",
+    "failure": true
+  },
+  {
+    "input": "http://[2001::1]",
+    "base": "http://example.org/foo/bar",
+    "href": "http://[2001::1]/",
+    "origin": "http://[2001::1]",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "[2001::1]",
+    "hostname": "[2001::1]",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://[::127.0.0.1]",
+    "base": "http://example.org/foo/bar",
+    "href": "http://[::7f00:1]/",
+    "origin": "http://[::7f00:1]",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "[::7f00:1]",
+    "hostname": "[::7f00:1]",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://[0:0:0:0:0:0:13.1.68.3]",
+    "base": "http://example.org/foo/bar",
+    "href": "http://[::d01:4403]/",
+    "origin": "http://[::d01:4403]",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "[::d01:4403]",
+    "hostname": "[::d01:4403]",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://[2001::1]:80",
+    "base": "http://example.org/foo/bar",
+    "href": "http://[2001::1]/",
+    "origin": "http://[2001::1]",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "[2001::1]",
+    "hostname": "[2001::1]",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/example.com/",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ftp:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "ftp://example.com/",
+    "origin": "ftp://example.com",
+    "protocol": "ftp:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "https:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "https://example.com/",
+    "origin": "https://example.com",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "madeupscheme:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "madeupscheme:/example.com/",
+    "origin": "null",
+    "protocol": "madeupscheme:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "file:///example.com/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file://example:1/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "file://example:test/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "file://example%/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "file://[example]/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "ftps:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "ftps:/example.com/",
+    "origin": "null",
+    "protocol": "ftps:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "gopher:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "gopher://example.com/",
+    "origin": "gopher://example.com",
+    "protocol": "gopher:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ws:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "ws://example.com/",
+    "origin": "ws://example.com",
+    "protocol": "ws:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "wss:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "wss://example.com/",
+    "origin": "wss://example.com",
+    "protocol": "wss:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "data:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "data:/example.com/",
+    "origin": "null",
+    "protocol": "data:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "javascript:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "javascript:/example.com/",
+    "origin": "null",
+    "protocol": "javascript:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "mailto:/example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "mailto:/example.com/",
+    "origin": "null",
+    "protocol": "mailto:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/example.com/",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ftp:example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "ftp://example.com/",
+    "origin": "ftp://example.com",
+    "protocol": "ftp:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "https:example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "https://example.com/",
+    "origin": "https://example.com",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "madeupscheme:example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "madeupscheme:example.com/",
+    "origin": "null",
+    "protocol": "madeupscheme:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ftps:example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "ftps:example.com/",
+    "origin": "null",
+    "protocol": "ftps:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "gopher:example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "gopher://example.com/",
+    "origin": "gopher://example.com",
+    "protocol": "gopher:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ws:example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "ws://example.com/",
+    "origin": "ws://example.com",
+    "protocol": "ws:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "wss:example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "wss://example.com/",
+    "origin": "wss://example.com",
+    "protocol": "wss:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "data:example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "data:example.com/",
+    "origin": "null",
+    "protocol": "data:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "javascript:example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "javascript:example.com/",
+    "origin": "null",
+    "protocol": "javascript:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "mailto:example.com/",
+    "base": "http://example.org/foo/bar",
+    "href": "mailto:example.com/",
+    "origin": "null",
+    "protocol": "mailto:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/a/b/c",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/a/b/c",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/a/b/c",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/a/ /c",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/a/%20/c",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/a/%20/c",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/a%2fc",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/a%2fc",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/a%2fc",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/a/%2f/c",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/a/%2f/c",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/a/%2f/c",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "#β",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/bar#%CE%B2",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "",
+    "hash": "#%CE%B2"
+  },
+  {
+    "input": "data:text/html,test#test",
+    "base": "http://example.org/foo/bar",
+    "href": "data:text/html,test#test",
+    "origin": "null",
+    "protocol": "data:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "text/html,test",
+    "search": "",
+    "hash": "#test"
+  },
+  {
+    "input": "tel:1234567890",
+    "base": "http://example.org/foo/bar",
+    "href": "tel:1234567890",
+    "origin": "null",
+    "protocol": "tel:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "1234567890",
+    "search": "",
+    "hash": ""
+  },
+  "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/file.html",
+  {
+    "input": "file:c:\\foo\\bar.html",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///c:/foo/bar.html",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/c:/foo/bar.html",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "  File:c|////foo\\bar.html",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///c:////foo/bar.html",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/c:////foo/bar.html",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "C|/foo/bar",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///C:/foo/bar",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/C|\\foo\\bar",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///C:/foo/bar",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "//C|/foo/bar",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///C:/foo/bar",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "//server/file",
+    "base": "file:///tmp/mock/path",
+    "href": "file://server/file",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "server",
+    "hostname": "server",
+    "port": "",
+    "pathname": "/file",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "\\\\server\\file",
+    "base": "file:///tmp/mock/path",
+    "href": "file://server/file",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "server",
+    "hostname": "server",
+    "port": "",
+    "pathname": "/file",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/\\server/file",
+    "base": "file:///tmp/mock/path",
+    "href": "file://server/file",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "server",
+    "hostname": "server",
+    "port": "",
+    "pathname": "/file",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:///foo/bar.txt",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///foo/bar.txt",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/foo/bar.txt",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:///home/me",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///home/me",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/home/me",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "//",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "///",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "///test",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///test",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/test",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file://test",
+    "base": "file:///tmp/mock/path",
+    "href": "file://test/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "test",
+    "hostname": "test",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file://localhost",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file://localhost/",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file://localhost/test",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///test",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/test",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "test",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///tmp/mock/test",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/tmp/mock/test",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:test",
+    "base": "file:///tmp/mock/path",
+    "href": "file:///tmp/mock/test",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/tmp/mock/test",
+    "search": "",
+    "hash": ""
+  },
+  "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/path.js",
+  {
+    "input": "http://example.com/././foo",
+    "base": "about:blank",
+    "href": "http://example.com/foo",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/./.foo",
+    "base": "about:blank",
+    "href": "http://example.com/.foo",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/.foo",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/.",
+    "base": "about:blank",
+    "href": "http://example.com/foo/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/./",
+    "base": "about:blank",
+    "href": "http://example.com/foo/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/bar/..",
+    "base": "about:blank",
+    "href": "http://example.com/foo/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/bar/../",
+    "base": "about:blank",
+    "href": "http://example.com/foo/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/..bar",
+    "base": "about:blank",
+    "href": "http://example.com/foo/..bar",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo/..bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/bar/../ton",
+    "base": "about:blank",
+    "href": "http://example.com/foo/ton",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo/ton",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/bar/../ton/../../a",
+    "base": "about:blank",
+    "href": "http://example.com/a",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/a",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/../../..",
+    "base": "about:blank",
+    "href": "http://example.com/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/../../../ton",
+    "base": "about:blank",
+    "href": "http://example.com/ton",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/ton",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/%2e",
+    "base": "about:blank",
+    "href": "http://example.com/foo/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/%2e%2",
+    "base": "about:blank",
+    "href": "http://example.com/foo/%2e%2",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo/%2e%2",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar",
+    "base": "about:blank",
+    "href": "http://example.com/%2e.bar",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/%2e.bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com////../..",
+    "base": "about:blank",
+    "href": "http://example.com//",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "//",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/bar//../..",
+    "base": "about:blank",
+    "href": "http://example.com/foo/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo/bar//..",
+    "base": "about:blank",
+    "href": "http://example.com/foo/bar/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo/bar/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo",
+    "base": "about:blank",
+    "href": "http://example.com/foo",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/%20foo",
+    "base": "about:blank",
+    "href": "http://example.com/%20foo",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/%20foo",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo%",
+    "base": "about:blank",
+    "href": "http://example.com/foo%",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo%",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo%2",
+    "base": "about:blank",
+    "href": "http://example.com/foo%2",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo%2",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo%2zbar",
+    "base": "about:blank",
+    "href": "http://example.com/foo%2zbar",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo%2zbar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo%2©zbar",
+    "base": "about:blank",
+    "href": "http://example.com/foo%2%C3%82%C2%A9zbar",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo%2%C3%82%C2%A9zbar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo%41%7a",
+    "base": "about:blank",
+    "href": "http://example.com/foo%41%7a",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo%41%7a",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo\t\u0091%91",
+    "base": "about:blank",
+    "href": "http://example.com/foo%C2%91%91",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo%C2%91%91",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo%00%51",
+    "base": "about:blank",
+    "href": "http://example.com/foo%00%51",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foo%00%51",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/(%28:%3A%29)",
+    "base": "about:blank",
+    "href": "http://example.com/(%28:%3A%29)",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/(%28:%3A%29)",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/%3A%3a%3C%3c",
+    "base": "about:blank",
+    "href": "http://example.com/%3A%3a%3C%3c",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/%3A%3a%3C%3c",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/foo\tbar",
+    "base": "about:blank",
+    "href": "http://example.com/foobar",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/foobar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com\\\\foo\\\\bar",
+    "base": "about:blank",
+    "href": "http://example.com//foo//bar",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "//foo//bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd",
+    "base": "about:blank",
+    "href": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/%7Ffp3%3Eju%3Dduvgw%3Dd",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/@asdf%40",
+    "base": "about:blank",
+    "href": "http://example.com/@asdf%40",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/@asdf%40",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/你好你好",
+    "base": "about:blank",
+    "href": "http://example.com/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/‥/foo",
+    "base": "about:blank",
+    "href": "http://example.com/%E2%80%A5/foo",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/%E2%80%A5/foo",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com//foo",
+    "base": "about:blank",
+    "href": "http://example.com/%EF%BB%BF/foo",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/%EF%BB%BF/foo",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.com/‮/foo/‭/bar",
+    "base": "about:blank",
+    "href": "http://example.com/%E2%80%AE/foo/%E2%80%AD/bar",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/%E2%80%AE/foo/%E2%80%AD/bar",
+    "search": "",
+    "hash": ""
+  },
+  "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/relative.js",
+  {
+    "input": "http://www.google.com/foo?bar=baz#",
+    "base": "about:blank",
+    "href": "http://www.google.com/foo?bar=baz#",
+    "origin": "http://www.google.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.google.com",
+    "hostname": "www.google.com",
+    "port": "",
+    "pathname": "/foo",
+    "search": "?bar=baz",
+    "hash": ""
+  },
+  {
+    "input": "http://www.google.com/foo?bar=baz# »",
+    "base": "about:blank",
+    "href": "http://www.google.com/foo?bar=baz# %C2%BB",
+    "origin": "http://www.google.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.google.com",
+    "hostname": "www.google.com",
+    "port": "",
+    "pathname": "/foo",
+    "search": "?bar=baz",
+    "hash": "# %C2%BB"
+  },
+  {
+    "input": "data:test# »",
+    "base": "about:blank",
+    "href": "data:test# %C2%BB",
+    "origin": "null",
+    "protocol": "data:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "test",
+    "search": "",
+    "hash": "# %C2%BB"
+  },
+  {
+    "input": "http://www.google.com",
+    "base": "about:blank",
+    "href": "http://www.google.com/",
+    "origin": "http://www.google.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.google.com",
+    "hostname": "www.google.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://192.0x00A80001",
+    "base": "about:blank",
+    "href": "http://192.168.0.1/",
+    "origin": "http://192.168.0.1",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "192.168.0.1",
+    "hostname": "192.168.0.1",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://www/foo%2Ehtml",
+    "base": "about:blank",
+    "href": "http://www/foo%2Ehtml",
+    "origin": "http://www",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www",
+    "hostname": "www",
+    "port": "",
+    "pathname": "/foo%2Ehtml",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://www/foo/%2E/html",
+    "base": "about:blank",
+    "href": "http://www/foo/html",
+    "origin": "http://www",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www",
+    "hostname": "www",
+    "port": "",
+    "pathname": "/foo/html",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://user:pass@/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http://%25DOMAIN:foobar@foodomain.com/",
+    "base": "about:blank",
+    "href": "http://%25DOMAIN:foobar@foodomain.com/",
+    "origin": "http://foodomain.com",
+    "protocol": "http:",
+    "username": "%25DOMAIN",
+    "password": "foobar",
+    "host": "foodomain.com",
+    "hostname": "foodomain.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:\\\\www.google.com\\foo",
+    "base": "about:blank",
+    "href": "http://www.google.com/foo",
+    "origin": "http://www.google.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.google.com",
+    "hostname": "www.google.com",
+    "port": "",
+    "pathname": "/foo",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://foo:80/",
+    "base": "about:blank",
+    "href": "http://foo/",
+    "origin": "http://foo",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "foo",
+    "hostname": "foo",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://foo:81/",
+    "base": "about:blank",
+    "href": "http://foo:81/",
+    "origin": "http://foo:81",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "foo:81",
+    "hostname": "foo",
+    "port": "81",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "httpa://foo:80/",
+    "base": "about:blank",
+    "href": "httpa://foo:80/",
+    "origin": "null",
+    "protocol": "httpa:",
+    "username": "",
+    "password": "",
+    "host": "foo:80",
+    "hostname": "foo",
+    "port": "80",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://foo:-80/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://foo:443/",
+    "base": "about:blank",
+    "href": "https://foo/",
+    "origin": "https://foo",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "foo",
+    "hostname": "foo",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "https://foo:80/",
+    "base": "about:blank",
+    "href": "https://foo:80/",
+    "origin": "https://foo:80",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "foo:80",
+    "hostname": "foo",
+    "port": "80",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ftp://foo:21/",
+    "base": "about:blank",
+    "href": "ftp://foo/",
+    "origin": "ftp://foo",
+    "protocol": "ftp:",
+    "username": "",
+    "password": "",
+    "host": "foo",
+    "hostname": "foo",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ftp://foo:80/",
+    "base": "about:blank",
+    "href": "ftp://foo:80/",
+    "origin": "ftp://foo:80",
+    "protocol": "ftp:",
+    "username": "",
+    "password": "",
+    "host": "foo:80",
+    "hostname": "foo",
+    "port": "80",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "gopher://foo:70/",
+    "base": "about:blank",
+    "href": "gopher://foo/",
+    "origin": "gopher://foo",
+    "protocol": "gopher:",
+    "username": "",
+    "password": "",
+    "host": "foo",
+    "hostname": "foo",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "gopher://foo:443/",
+    "base": "about:blank",
+    "href": "gopher://foo:443/",
+    "origin": "gopher://foo:443",
+    "protocol": "gopher:",
+    "username": "",
+    "password": "",
+    "host": "foo:443",
+    "hostname": "foo",
+    "port": "443",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ws://foo:80/",
+    "base": "about:blank",
+    "href": "ws://foo/",
+    "origin": "ws://foo",
+    "protocol": "ws:",
+    "username": "",
+    "password": "",
+    "host": "foo",
+    "hostname": "foo",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ws://foo:81/",
+    "base": "about:blank",
+    "href": "ws://foo:81/",
+    "origin": "ws://foo:81",
+    "protocol": "ws:",
+    "username": "",
+    "password": "",
+    "host": "foo:81",
+    "hostname": "foo",
+    "port": "81",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ws://foo:443/",
+    "base": "about:blank",
+    "href": "ws://foo:443/",
+    "origin": "ws://foo:443",
+    "protocol": "ws:",
+    "username": "",
+    "password": "",
+    "host": "foo:443",
+    "hostname": "foo",
+    "port": "443",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ws://foo:815/",
+    "base": "about:blank",
+    "href": "ws://foo:815/",
+    "origin": "ws://foo:815",
+    "protocol": "ws:",
+    "username": "",
+    "password": "",
+    "host": "foo:815",
+    "hostname": "foo",
+    "port": "815",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "wss://foo:80/",
+    "base": "about:blank",
+    "href": "wss://foo:80/",
+    "origin": "wss://foo:80",
+    "protocol": "wss:",
+    "username": "",
+    "password": "",
+    "host": "foo:80",
+    "hostname": "foo",
+    "port": "80",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "wss://foo:81/",
+    "base": "about:blank",
+    "href": "wss://foo:81/",
+    "origin": "wss://foo:81",
+    "protocol": "wss:",
+    "username": "",
+    "password": "",
+    "host": "foo:81",
+    "hostname": "foo",
+    "port": "81",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "wss://foo:443/",
+    "base": "about:blank",
+    "href": "wss://foo/",
+    "origin": "wss://foo",
+    "protocol": "wss:",
+    "username": "",
+    "password": "",
+    "host": "foo",
+    "hostname": "foo",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "wss://foo:815/",
+    "base": "about:blank",
+    "href": "wss://foo:815/",
+    "origin": "wss://foo:815",
+    "protocol": "wss:",
+    "username": "",
+    "password": "",
+    "host": "foo:815",
+    "hostname": "foo",
+    "port": "815",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:/example.com/",
+    "base": "about:blank",
+    "href": "http://example.com/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ftp:/example.com/",
+    "base": "about:blank",
+    "href": "ftp://example.com/",
+    "origin": "ftp://example.com",
+    "protocol": "ftp:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "https:/example.com/",
+    "base": "about:blank",
+    "href": "https://example.com/",
+    "origin": "https://example.com",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "madeupscheme:/example.com/",
+    "base": "about:blank",
+    "href": "madeupscheme:/example.com/",
+    "origin": "null",
+    "protocol": "madeupscheme:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:/example.com/",
+    "base": "about:blank",
+    "href": "file:///example.com/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ftps:/example.com/",
+    "base": "about:blank",
+    "href": "ftps:/example.com/",
+    "origin": "null",
+    "protocol": "ftps:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "gopher:/example.com/",
+    "base": "about:blank",
+    "href": "gopher://example.com/",
+    "origin": "gopher://example.com",
+    "protocol": "gopher:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ws:/example.com/",
+    "base": "about:blank",
+    "href": "ws://example.com/",
+    "origin": "ws://example.com",
+    "protocol": "ws:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "wss:/example.com/",
+    "base": "about:blank",
+    "href": "wss://example.com/",
+    "origin": "wss://example.com",
+    "protocol": "wss:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "data:/example.com/",
+    "base": "about:blank",
+    "href": "data:/example.com/",
+    "origin": "null",
+    "protocol": "data:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "javascript:/example.com/",
+    "base": "about:blank",
+    "href": "javascript:/example.com/",
+    "origin": "null",
+    "protocol": "javascript:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "mailto:/example.com/",
+    "base": "about:blank",
+    "href": "mailto:/example.com/",
+    "origin": "null",
+    "protocol": "mailto:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:example.com/",
+    "base": "about:blank",
+    "href": "http://example.com/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ftp:example.com/",
+    "base": "about:blank",
+    "href": "ftp://example.com/",
+    "origin": "ftp://example.com",
+    "protocol": "ftp:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "https:example.com/",
+    "base": "about:blank",
+    "href": "https://example.com/",
+    "origin": "https://example.com",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "madeupscheme:example.com/",
+    "base": "about:blank",
+    "href": "madeupscheme:example.com/",
+    "origin": "null",
+    "protocol": "madeupscheme:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ftps:example.com/",
+    "base": "about:blank",
+    "href": "ftps:example.com/",
+    "origin": "null",
+    "protocol": "ftps:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "gopher:example.com/",
+    "base": "about:blank",
+    "href": "gopher://example.com/",
+    "origin": "gopher://example.com",
+    "protocol": "gopher:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ws:example.com/",
+    "base": "about:blank",
+    "href": "ws://example.com/",
+    "origin": "ws://example.com",
+    "protocol": "ws:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "wss:example.com/",
+    "base": "about:blank",
+    "href": "wss://example.com/",
+    "origin": "wss://example.com",
+    "protocol": "wss:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "data:example.com/",
+    "base": "about:blank",
+    "href": "data:example.com/",
+    "origin": "null",
+    "protocol": "data:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "javascript:example.com/",
+    "base": "about:blank",
+    "href": "javascript:example.com/",
+    "origin": "null",
+    "protocol": "javascript:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "example.com/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "mailto:example.com/",
+    "base": "about:blank",
+    "href": "mailto:example.com/",
+    "origin": "null",
+    "protocol": "mailto:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "example.com/",
+    "search": "",
+    "hash": ""
+  },
+  "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/segments-userinfo-vs-host.html",
+  {
+    "input": "http:@www.example.com",
+    "base": "about:blank",
+    "href": "http://www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:/@www.example.com",
+    "base": "about:blank",
+    "href": "http://www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://@www.example.com",
+    "base": "about:blank",
+    "href": "http://www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:a:b@www.example.com",
+    "base": "about:blank",
+    "href": "http://a:b@www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "a",
+    "password": "b",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:/a:b@www.example.com",
+    "base": "about:blank",
+    "href": "http://a:b@www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "a",
+    "password": "b",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://a:b@www.example.com",
+    "base": "about:blank",
+    "href": "http://a:b@www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "a",
+    "password": "b",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://@pple.com",
+    "base": "about:blank",
+    "href": "http://pple.com/",
+    "origin": "http://pple.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "pple.com",
+    "hostname": "pple.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http::b@www.example.com",
+    "base": "about:blank",
+    "href": "http://:b@www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "b",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:/:b@www.example.com",
+    "base": "about:blank",
+    "href": "http://:b@www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "b",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://:b@www.example.com",
+    "base": "about:blank",
+    "href": "http://:b@www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "b",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:/:@/www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http://user@/www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http:@/www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http:/@/www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http://@/www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https:@/www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http:a:b@/www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http:/a:b@/www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http://a:b@/www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http::@/www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http:a:@www.example.com",
+    "base": "about:blank",
+    "href": "http://a@www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "a",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:/a:@www.example.com",
+    "base": "about:blank",
+    "href": "http://a@www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "a",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://a:@www.example.com",
+    "base": "about:blank",
+    "href": "http://a@www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "a",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://www.@pple.com",
+    "base": "about:blank",
+    "href": "http://www.@pple.com/",
+    "origin": "http://pple.com",
+    "protocol": "http:",
+    "username": "www.",
+    "password": "",
+    "host": "pple.com",
+    "hostname": "pple.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http:@:www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http:/@:www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http://@:www.example.com",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http://:@www.example.com",
+    "base": "about:blank",
+    "href": "http://www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "# Others",
+  {
+    "input": "/",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/test.txt",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example.com/test.txt",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/test.txt",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": ".",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "..",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example.com/",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "test.txt",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example.com/test.txt",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/test.txt",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "./test.txt",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example.com/test.txt",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/test.txt",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "../test.txt",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example.com/test.txt",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/test.txt",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "../aaa/test.txt",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example.com/aaa/test.txt",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/aaa/test.txt",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "../../test.txt",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example.com/test.txt",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/test.txt",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "中/test.txt",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example.com/%E4%B8%AD/test.txt",
+    "origin": "http://www.example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example.com",
+    "hostname": "www.example.com",
+    "port": "",
+    "pathname": "/%E4%B8%AD/test.txt",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://www.example2.com",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example2.com/",
+    "origin": "http://www.example2.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example2.com",
+    "hostname": "www.example2.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "//www.example2.com",
+    "base": "http://www.example.com/test",
+    "href": "http://www.example2.com/",
+    "origin": "http://www.example2.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.example2.com",
+    "hostname": "www.example2.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:...",
+    "base": "http://www.example.com/test",
+    "href": "file:///...",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/...",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:..",
+    "base": "http://www.example.com/test",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:a",
+    "base": "http://www.example.com/test",
+    "href": "file:///a",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/a",
+    "search": "",
+    "hash": ""
+  },
+  "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/host.html",
+  "Basic canonicalization, uppercase should be converted to lowercase",
+  {
+    "input": "http://ExAmPlE.CoM",
+    "base": "http://other.com/",
+    "href": "http://example.com/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example example.com",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://Goo%20 goo%7C|.com",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://[]",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://[:]",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  "U+3000 is mapped to U+0020 (space) which is disallowed",
+  {
+    "input": "http://GOO\u00a0\u3000goo.com",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  "Other types of space (no-break, zero-width, zero-width-no-break) are name-prepped away to nothing. U+200B, U+2060, and U+FEFF, are ignored",
+  {
+    "input": "http://GOO\u200b\u2060\ufeffgoo.com",
+    "base": "http://other.com/",
+    "href": "http://googoo.com/",
+    "origin": "http://googoo.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "googoo.com",
+    "hostname": "googoo.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "Leading and trailing C0 control or space",
+  {
+    "input": "\u0000\u001b\u0004\u0012 http://example.com/\u001f \u000d ",
+    "base": "about:blank",
+    "href": "http://example.com/",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "Ideographic full stop (full-width period for Chinese, etc.) should be treated as a dot. U+3002 is mapped to U+002E (dot)",
+  {
+    "input": "http://www.foo。bar.com",
+    "base": "http://other.com/",
+    "href": "http://www.foo.bar.com/",
+    "origin": "http://www.foo.bar.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "www.foo.bar.com",
+    "hostname": "www.foo.bar.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "Invalid unicode characters should fail... U+FDD0 is disallowed; %ef%b7%90 is U+FDD0",
+  {
+    "input": "http://\ufdd0zyx.com",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  "This is the same as previous but escaped",
+  {
+    "input": "http://%ef%b7%90zyx.com",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  "U+FFFD",
+  {
+    "input": "https://\ufffd",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://%EF%BF%BD",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://x/\ufffd?\ufffd#\ufffd",
+    "base": "about:blank",
+    "href": "https://x/%EF%BF%BD?%EF%BF%BD#%EF%BF%BD",
+    "origin": "https://x",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "x",
+    "hostname": "x",
+    "port": "",
+    "pathname": "/%EF%BF%BD",
+    "search": "?%EF%BF%BD",
+    "hash": "#%EF%BF%BD"
+  },
+  "Test name prepping, fullwidth input should be converted to ASCII and NOT IDN-ized. This is 'Go' in fullwidth UTF-8/UTF-16.",
+  {
+    "input": "http://Go.com",
+    "base": "http://other.com/",
+    "href": "http://go.com/",
+    "origin": "http://go.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "go.com",
+    "hostname": "go.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "URL spec forbids the following. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24257",
+  {
+    "input": "http://%41.com",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://%ef%bc%85%ef%bc%94%ef%bc%91.com",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  "...%00 in fullwidth should fail (also as escaped UTF-8 input)",
+  {
+    "input": "http://%00.com",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://%ef%bc%85%ef%bc%90%ef%bc%90.com",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  "Basic IDN support, UTF-8 and UTF-16 input should be converted to IDN",
+  {
+    "input": "http://你好你好",
+    "base": "http://other.com/",
+    "href": "http://xn--6qqa088eba/",
+    "origin": "http://xn--6qqa088eba",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "xn--6qqa088eba",
+    "hostname": "xn--6qqa088eba",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "https://faß.ExAmPlE/",
+    "base": "about:blank",
+    "href": "https://xn--fa-hia.example/",
+    "origin": "https://xn--fa-hia.example",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "xn--fa-hia.example",
+    "hostname": "xn--fa-hia.example",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "sc://faß.ExAmPlE/",
+    "base": "about:blank",
+    "href": "sc://fa%C3%9F.ExAmPlE/",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "fa%C3%9F.ExAmPlE",
+    "hostname": "fa%C3%9F.ExAmPlE",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "Invalid escaped characters should fail and the percents should be escaped. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24191",
+  {
+    "input": "http://%zz%66%a.com",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  "If we get an invalid character that has been escaped.",
+  {
+    "input": "http://%25",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://hello%00",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  "Escaped numbers should be treated like IP addresses if they are.",
+  {
+    "input": "http://%30%78%63%30%2e%30%32%35%30.01",
+    "base": "http://other.com/",
+    "href": "http://192.168.0.1/",
+    "origin": "http://192.168.0.1",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "192.168.0.1",
+    "hostname": "192.168.0.1",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://%30%78%63%30%2e%30%32%35%30.01%2e",
+    "base": "http://other.com/",
+    "href": "http://192.168.0.1/",
+    "origin": "http://192.168.0.1",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "192.168.0.1",
+    "hostname": "192.168.0.1",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://192.168.0.257",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  "Invalid escaping in hosts causes failure",
+  {
+    "input": "http://%3g%78%63%30%2e%30%32%35%30%2E.01",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  "A space in a host causes failure",
+  {
+    "input": "http://192.168.0.1 hello",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "https://x x:12",
+    "base": "about:blank",
+    "failure": true
+  },
+  "Fullwidth and escaped UTF-8 fullwidth should still be treated as IP",
+  {
+    "input": "http://0Xc0.0250.01",
+    "base": "http://other.com/",
+    "href": "http://192.168.0.1/",
+    "origin": "http://192.168.0.1",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "192.168.0.1",
+    "hostname": "192.168.0.1",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "Domains with empty labels",
+  {
+    "input": "http://./",
+    "base": "about:blank",
+    "href": "http://./",
+    "origin": "http://.",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": ".",
+    "hostname": ".",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://../",
+    "base": "about:blank",
+    "href": "http://../",
+    "origin": "http://..",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "..",
+    "hostname": "..",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://0..0x300/",
+    "base": "about:blank",
+    "href": "http://0..0x300/",
+    "origin": "http://0..0x300",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "0..0x300",
+    "hostname": "0..0x300",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "Broken IPv6",
+  {
+    "input": "http://[www.google.com]/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http://[google.com]",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://[::1.2.3.4x]",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://[::1.2.3.]",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://[::1.2.]",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://[::1.]",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  "Misc Unicode",
+  {
+    "input": "http://foo:💩@example.com/bar",
+    "base": "http://other.com/",
+    "href": "http://foo:%F0%9F%92%A9@example.com/bar",
+    "origin": "http://example.com",
+    "protocol": "http:",
+    "username": "foo",
+    "password": "%F0%9F%92%A9",
+    "host": "example.com",
+    "hostname": "example.com",
+    "port": "",
+    "pathname": "/bar",
+    "search": "",
+    "hash": ""
+  },
+  "# resolving a fragment against any scheme succeeds",
+  {
+    "input": "#",
+    "base": "test:test",
+    "href": "test:test#",
+    "origin": "null",
+    "protocol": "test:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "test",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "#x",
+    "base": "mailto:x@x.com",
+    "href": "mailto:x@x.com#x",
+    "origin": "null",
+    "protocol": "mailto:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "x@x.com",
+    "search": "",
+    "hash": "#x"
+  },
+  {
+    "input": "#x",
+    "base": "data:,",
+    "href": "data:,#x",
+    "origin": "null",
+    "protocol": "data:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": ",",
+    "search": "",
+    "hash": "#x"
+  },
+  {
+    "input": "#x",
+    "base": "about:blank",
+    "href": "about:blank#x",
+    "origin": "null",
+    "protocol": "about:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "blank",
+    "search": "",
+    "hash": "#x"
+  },
+  {
+    "input": "#",
+    "base": "test:test?test",
+    "href": "test:test?test#",
+    "origin": "null",
+    "protocol": "test:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "test",
+    "search": "?test",
+    "hash": ""
+  },
+  "# multiple @ in authority state",
+  {
+    "input": "https://@test@test@example:800/",
+    "base": "http://doesnotmatter/",
+    "href": "https://%40test%40test@example:800/",
+    "origin": "https://example:800",
+    "protocol": "https:",
+    "username": "%40test%40test",
+    "password": "",
+    "host": "example:800",
+    "hostname": "example",
+    "port": "800",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "https://@@@example",
+    "base": "http://doesnotmatter/",
+    "href": "https://%40%40@example/",
+    "origin": "https://example",
+    "protocol": "https:",
+    "username": "%40%40",
+    "password": "",
+    "host": "example",
+    "hostname": "example",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "non-az-09 characters",
+  {
+    "input": "http://`{}:`{}@h/`{}?`{}",
+    "base": "http://doesnotmatter/",
+    "href": "http://%60%7B%7D:%60%7B%7D@h/%60%7B%7D?`{}",
+    "origin": "http://h",
+    "protocol": "http:",
+    "username": "%60%7B%7D",
+    "password": "%60%7B%7D",
+    "host": "h",
+    "hostname": "h",
+    "port": "",
+    "pathname": "/%60%7B%7D",
+    "search": "?`{}",
+    "hash": ""
+  },
+  "# Credentials in base",
+  {
+    "input": "/some/path",
+    "base": "http://user@example.org/smth",
+    "href": "http://user@example.org/some/path",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "user",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/some/path",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "",
+    "base": "http://user:pass@example.org:21/smth",
+    "href": "http://user:pass@example.org:21/smth",
+    "origin": "http://example.org:21",
+    "protocol": "http:",
+    "username": "user",
+    "password": "pass",
+    "host": "example.org:21",
+    "hostname": "example.org",
+    "port": "21",
+    "pathname": "/smth",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/some/path",
+    "base": "http://user:pass@example.org:21/smth",
+    "href": "http://user:pass@example.org:21/some/path",
+    "origin": "http://example.org:21",
+    "protocol": "http:",
+    "username": "user",
+    "password": "pass",
+    "host": "example.org:21",
+    "hostname": "example.org",
+    "port": "21",
+    "pathname": "/some/path",
+    "search": "",
+    "hash": ""
+  },
+  "# a set of tests designed by zcorpan for relative URLs with unknown schemes",
+  {
+    "input": "i",
+    "base": "sc:sd",
+    "failure": true
+  },
+  {
+    "input": "i",
+    "base": "sc:sd/sd",
+    "failure": true
+  },
+  {
+    "input": "i",
+    "base": "sc:/pa/pa",
+    "href": "sc:/pa/i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/pa/i",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "i",
+    "base": "sc://ho/pa",
+    "href": "sc://ho/i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "ho",
+    "hostname": "ho",
+    "port": "",
+    "pathname": "/i",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "i",
+    "base": "sc:///pa/pa",
+    "href": "sc:///pa/i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/pa/i",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "../i",
+    "base": "sc:sd",
+    "failure": true
+  },
+  {
+    "input": "../i",
+    "base": "sc:sd/sd",
+    "failure": true
+  },
+  {
+    "input": "../i",
+    "base": "sc:/pa/pa",
+    "href": "sc:/i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/i",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "../i",
+    "base": "sc://ho/pa",
+    "href": "sc://ho/i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "ho",
+    "hostname": "ho",
+    "port": "",
+    "pathname": "/i",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "../i",
+    "base": "sc:///pa/pa",
+    "href": "sc:///i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/i",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/i",
+    "base": "sc:sd",
+    "failure": true
+  },
+  {
+    "input": "/i",
+    "base": "sc:sd/sd",
+    "failure": true
+  },
+  {
+    "input": "/i",
+    "base": "sc:/pa/pa",
+    "href": "sc:/i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/i",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/i",
+    "base": "sc://ho/pa",
+    "href": "sc://ho/i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "ho",
+    "hostname": "ho",
+    "port": "",
+    "pathname": "/i",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/i",
+    "base": "sc:///pa/pa",
+    "href": "sc:///i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/i",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "?i",
+    "base": "sc:sd",
+    "failure": true
+  },
+  {
+    "input": "?i",
+    "base": "sc:sd/sd",
+    "failure": true
+  },
+  {
+    "input": "?i",
+    "base": "sc:/pa/pa",
+    "href": "sc:/pa/pa?i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/pa/pa",
+    "search": "?i",
+    "hash": ""
+  },
+  {
+    "input": "?i",
+    "base": "sc://ho/pa",
+    "href": "sc://ho/pa?i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "ho",
+    "hostname": "ho",
+    "port": "",
+    "pathname": "/pa",
+    "search": "?i",
+    "hash": ""
+  },
+  {
+    "input": "?i",
+    "base": "sc:///pa/pa",
+    "href": "sc:///pa/pa?i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/pa/pa",
+    "search": "?i",
+    "hash": ""
+  },
+  {
+    "input": "#i",
+    "base": "sc:sd",
+    "href": "sc:sd#i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "sd",
+    "search": "",
+    "hash": "#i"
+  },
+  {
+    "input": "#i",
+    "base": "sc:sd/sd",
+    "href": "sc:sd/sd#i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "sd/sd",
+    "search": "",
+    "hash": "#i"
+  },
+  {
+    "input": "#i",
+    "base": "sc:/pa/pa",
+    "href": "sc:/pa/pa#i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/pa/pa",
+    "search": "",
+    "hash": "#i"
+  },
+  {
+    "input": "#i",
+    "base": "sc://ho/pa",
+    "href": "sc://ho/pa#i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "ho",
+    "hostname": "ho",
+    "port": "",
+    "pathname": "/pa",
+    "search": "",
+    "hash": "#i"
+  },
+  {
+    "input": "#i",
+    "base": "sc:///pa/pa",
+    "href": "sc:///pa/pa#i",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/pa/pa",
+    "search": "",
+    "hash": "#i"
+  },
+  "# make sure that relative URL logic works on known typically non-relative schemes too",
+  {
+    "input": "about:/../",
+    "base": "about:blank",
+    "href": "about:/",
+    "origin": "null",
+    "protocol": "about:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "data:/../",
+    "base": "about:blank",
+    "href": "data:/",
+    "origin": "null",
+    "protocol": "data:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "javascript:/../",
+    "base": "about:blank",
+    "href": "javascript:/",
+    "origin": "null",
+    "protocol": "javascript:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "mailto:/../",
+    "base": "about:blank",
+    "href": "mailto:/",
+    "origin": "null",
+    "protocol": "mailto:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "# unknown schemes and their hosts",
+  {
+    "input": "sc://ñ.test/",
+    "base": "about:blank",
+    "href": "sc://%C3%B1.test/",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "%C3%B1.test",
+    "hostname": "%C3%B1.test",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "sc://\u001F!\"$&'()*+,-.;<=>^_`{|}~/",
+    "base": "about:blank",
+    "href": "sc://%1F!\"$&'()*+,-.;<=>^_`{|}~/",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "%1F!\"$&'()*+,-.;<=>^_`{|}~",
+    "hostname": "%1F!\"$&'()*+,-.;<=>^_`{|}~",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "sc://\u0000/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "sc:// /",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "sc://%/",
+    "base": "about:blank",
+    "href": "sc://%/",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "%",
+    "hostname": "%",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "sc://@/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "sc://te@s:t@/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "sc://:/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "sc://:12/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "sc://[/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "sc://\\/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "sc://]/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "x",
+    "base": "sc://ñ",
+    "href": "sc://%C3%B1/x",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "%C3%B1",
+    "hostname": "%C3%B1",
+    "port": "",
+    "pathname": "/x",
+    "search": "",
+    "hash": ""
+  },
+  "# unknown schemes and backslashes",
+  {
+    "input": "sc:\\../",
+    "base": "about:blank",
+    "href": "sc:\\../",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "\\../",
+    "search": "",
+    "hash": ""
+  },
+  "# unknown scheme with path looking like a password",
+  {
+    "input": "sc::a@example.net",
+    "base": "about:blank",
+    "href": "sc::a@example.net",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": ":a@example.net",
+    "search": "",
+    "hash": ""
+  },
+  "# unknown scheme with bogus percent-encoding",
+  {
+    "input": "wow:%NBD",
+    "base": "about:blank",
+    "href": "wow:%NBD",
+    "origin": "null",
+    "protocol": "wow:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "%NBD",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "wow:%1G",
+    "base": "about:blank",
+    "href": "wow:%1G",
+    "origin": "null",
+    "protocol": "wow:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "%1G",
+    "search": "",
+    "hash": ""
+  },
+  "# Hosts and percent-encoding",
+  {
+    "input": "ftp://example.com%80/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "ftp://example.com%A0/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://example.com%80/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://example.com%A0/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "ftp://%e2%98%83",
+    "base": "about:blank",
+    "href": "ftp://xn--n3h/",
+    "origin": "ftp://xn--n3h",
+    "protocol": "ftp:",
+    "username": "",
+    "password": "",
+    "host": "xn--n3h",
+    "hostname": "xn--n3h",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "https://%e2%98%83",
+    "base": "about:blank",
+    "href": "https://xn--n3h/",
+    "origin": "https://xn--n3h",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "xn--n3h",
+    "hostname": "xn--n3h",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "# tests from jsdom/whatwg-url designed for code coverage",
+  {
+    "input": "http://127.0.0.1:10100/relative_import.html",
+    "base": "about:blank",
+    "href": "http://127.0.0.1:10100/relative_import.html",
+    "origin": "http://127.0.0.1:10100",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "127.0.0.1:10100",
+    "hostname": "127.0.0.1",
+    "port": "10100",
+    "pathname": "/relative_import.html",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://facebook.com/?foo=%7B%22abc%22",
+    "base": "about:blank",
+    "href": "http://facebook.com/?foo=%7B%22abc%22",
+    "origin": "http://facebook.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "facebook.com",
+    "hostname": "facebook.com",
+    "port": "",
+    "pathname": "/",
+    "search": "?foo=%7B%22abc%22",
+    "hash": ""
+  },
+  {
+    "input": "https://localhost:3000/jqueryui@1.2.3",
+    "base": "about:blank",
+    "href": "https://localhost:3000/jqueryui@1.2.3",
+    "origin": "https://localhost:3000",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "localhost:3000",
+    "hostname": "localhost",
+    "port": "3000",
+    "pathname": "/jqueryui@1.2.3",
+    "search": "",
+    "hash": ""
+  },
+  "# tab/LF/CR",
+  {
+    "input": "h\tt\nt\rp://h\to\ns\rt:9\t0\n0\r0/p\ta\nt\rh?q\tu\ne\rry#f\tr\na\rg",
+    "base": "about:blank",
+    "href": "http://host:9000/path?query#frag",
+    "origin": "http://host:9000",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "host:9000",
+    "hostname": "host",
+    "port": "9000",
+    "pathname": "/path",
+    "search": "?query",
+    "hash": "#frag"
+  },
+  "# Stringification of URL.searchParams",
+  {
+    "input": "?a=b&c=d",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/bar?a=b&c=d",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "?a=b&c=d",
+    "searchParams": "a=b&c=d",
+    "hash": ""
+  },
+  {
+    "input": "??a=b&c=d",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/bar??a=b&c=d",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "??a=b&c=d",
+    "searchParams": "%3Fa=b&c=d",
+    "hash": ""
+  },
+  "# Scheme only",
+  {
+    "input": "http:",
+    "base": "http://example.org/foo/bar",
+    "href": "http://example.org/foo/bar",
+    "origin": "http://example.org",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "",
+    "searchParams": "",
+    "hash": ""
+  },
+  {
+    "input": "http:",
+    "base": "https://example.org/foo/bar",
+    "failure": true
+  },
+  {
+    "input": "sc:",
+    "base": "https://example.org/foo/bar",
+    "href": "sc:",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "",
+    "search": "",
+    "searchParams": "",
+    "hash": ""
+  },
+  "# Percent encoding of fragments",
+  {
+    "input": "http://foo.bar/baz?qux#foo\bbar",
+    "base": "about:blank",
+    "href": "http://foo.bar/baz?qux#foo%08bar",
+    "origin": "http://foo.bar",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "foo.bar",
+    "hostname": "foo.bar",
+    "port": "",
+    "pathname": "/baz",
+    "search": "?qux",
+    "searchParams": "qux=",
+    "hash": "#foo%08bar"
+  },
+  "# IPv4 parsing (via https://github.com/nodejs/node/pull/10317)",
+  {
+    "input": "http://192.168.257",
+    "base": "http://other.com/",
+    "href": "http://192.168.1.1/",
+    "origin": "http://192.168.1.1",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "192.168.1.1",
+    "hostname": "192.168.1.1",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://192.168.257.com",
+    "base": "http://other.com/",
+    "href": "http://192.168.257.com/",
+    "origin": "http://192.168.257.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "192.168.257.com",
+    "hostname": "192.168.257.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://256",
+    "base": "http://other.com/",
+    "href": "http://0.0.1.0/",
+    "origin": "http://0.0.1.0",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "0.0.1.0",
+    "hostname": "0.0.1.0",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://256.com",
+    "base": "http://other.com/",
+    "href": "http://256.com/",
+    "origin": "http://256.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "256.com",
+    "hostname": "256.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://999999999",
+    "base": "http://other.com/",
+    "href": "http://59.154.201.255/",
+    "origin": "http://59.154.201.255",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "59.154.201.255",
+    "hostname": "59.154.201.255",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://999999999.com",
+    "base": "http://other.com/",
+    "href": "http://999999999.com/",
+    "origin": "http://999999999.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "999999999.com",
+    "hostname": "999999999.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://10000000000",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://10000000000.com",
+    "base": "http://other.com/",
+    "href": "http://10000000000.com/",
+    "origin": "http://10000000000.com",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "10000000000.com",
+    "hostname": "10000000000.com",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://4294967295",
+    "base": "http://other.com/",
+    "href": "http://255.255.255.255/",
+    "origin": "http://255.255.255.255",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "255.255.255.255",
+    "hostname": "255.255.255.255",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://4294967296",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://0xffffffff",
+    "base": "http://other.com/",
+    "href": "http://255.255.255.255/",
+    "origin": "http://255.255.255.255",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "255.255.255.255",
+    "hostname": "255.255.255.255",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://0xffffffff1",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://256.256.256.256",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://256.256.256.256.256",
+    "base": "http://other.com/",
+    "href": "http://256.256.256.256.256/",
+    "origin": "http://256.256.256.256.256",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "256.256.256.256.256",
+    "hostname": "256.256.256.256.256",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "https://0x.0x.0",
+    "base": "about:blank",
+    "href": "https://0.0.0.0/",
+    "origin": "https://0.0.0.0",
+    "protocol": "https:",
+    "username": "",
+    "password": "",
+    "host": "0.0.0.0",
+    "hostname": "0.0.0.0",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "More IPv4 parsing (via https://github.com/jsdom/whatwg-url/issues/92)",
+  {
+    "input": "https://0x100000000/test",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://256.0.0.1/test",
+    "base": "about:blank",
+    "failure": true
+  },
+  "# file URLs containing percent-encoded Windows drive letters (shouldn't work)",
+  {
+    "input": "file:///C%3A/",
+    "base": "about:blank",
+    "href": "file:///C%3A/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C%3A/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:///C%7C/",
+    "base": "about:blank",
+    "href": "file:///C%7C/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C%7C/",
+    "search": "",
+    "hash": ""
+  },
+  "# file URLs relative to other file URLs (via https://github.com/jsdom/whatwg-url/pull/60)",
+  {
+    "input": "pix/submit.gif",
+    "base": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/anchor.html",
+    "href": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "..",
+    "base": "file:///C:/",
+    "href": "file:///C:/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "..",
+    "base": "file:///",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "# More file URL tests by zcorpan and annevk",
+  {
+    "input": "/",
+    "base": "file:///C:/a/b",
+    "href": "file:///C:/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "//d:",
+    "base": "file:///C:/a/b",
+    "href": "file:///d:",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/d:",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "//d:/..",
+    "base": "file:///C:/a/b",
+    "href": "file:///d:/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/d:/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "..",
+    "base": "file:///ab:/",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "..",
+    "base": "file:///1:/",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "",
+    "base": "file:///test?test#test",
+    "href": "file:///test?test",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/test",
+    "search": "?test",
+    "hash": ""
+  },
+  {
+    "input": "file:",
+    "base": "file:///test?test#test",
+    "href": "file:///test?test",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/test",
+    "search": "?test",
+    "hash": ""
+  },
+  {
+    "input": "?x",
+    "base": "file:///test?test#test",
+    "href": "file:///test?x",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/test",
+    "search": "?x",
+    "hash": ""
+  },
+  {
+    "input": "file:?x",
+    "base": "file:///test?test#test",
+    "href": "file:///test?x",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/test",
+    "search": "?x",
+    "hash": ""
+  },
+  {
+    "input": "#x",
+    "base": "file:///test?test#test",
+    "href": "file:///test?test#x",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/test",
+    "search": "?test",
+    "hash": "#x"
+  },
+  {
+    "input": "file:#x",
+    "base": "file:///test?test#test",
+    "href": "file:///test?test#x",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/test",
+    "search": "?test",
+    "hash": "#x"
+  },
+  "# File URLs and many (back)slashes",
+  {
+    "input": "file:\\\\//",
+    "base": "about:blank",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:\\\\\\\\",
+    "base": "about:blank",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:\\\\\\\\?fox",
+    "base": "about:blank",
+    "href": "file:///?fox",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "?fox",
+    "hash": ""
+  },
+  {
+    "input": "file:\\\\\\\\#guppy",
+    "base": "about:blank",
+    "href": "file:///#guppy",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": "#guppy"
+  },
+  {
+    "input": "file://spider///",
+    "base": "about:blank",
+    "href": "file://spider/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "spider",
+    "hostname": "spider",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:\\\\localhost//",
+    "base": "about:blank",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:///localhost//cat",
+    "base": "about:blank",
+    "href": "file:///localhost//cat",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/localhost//cat",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file://\\/localhost//cat",
+    "base": "about:blank",
+    "href": "file:///localhost//cat",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/localhost//cat",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file://localhost//a//../..//",
+    "base": "about:blank",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/////mouse",
+    "base": "file:///elephant",
+    "href": "file:///mouse",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/mouse",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "\\//pig",
+    "base": "file://lion/",
+    "href": "file:///pig",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/pig",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "\\/localhost//pig",
+    "base": "file://lion/",
+    "href": "file:///pig",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/pig",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "//localhost//pig",
+    "base": "file://lion/",
+    "href": "file:///pig",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/pig",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/..//localhost//pig",
+    "base": "file://lion/",
+    "href": "file://lion/localhost//pig",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "lion",
+    "hostname": "lion",
+    "port": "",
+    "pathname": "/localhost//pig",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file://",
+    "base": "file://ape/",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "# File URLs with non-empty hosts",
+  {
+    "input": "/rooibos",
+    "base": "file://tea/",
+    "href": "file://tea/rooibos",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "tea",
+    "hostname": "tea",
+    "port": "",
+    "pathname": "/rooibos",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/?chai",
+    "base": "file://tea/",
+    "href": "file://tea/?chai",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "tea",
+    "hostname": "tea",
+    "port": "",
+    "pathname": "/",
+    "search": "?chai",
+    "hash": ""
+  },
+  "# Windows drive letter handling with the 'file:' base URL",
+  {
+    "input": "C|",
+    "base": "file://host/dir/file",
+    "href": "file:///C:",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "C|#",
+    "base": "file://host/dir/file",
+    "href": "file:///C:#",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "C|?",
+    "base": "file://host/dir/file",
+    "href": "file:///C:?",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "C|/",
+    "base": "file://host/dir/file",
+    "href": "file:///C:/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "C|\n/",
+    "base": "file://host/dir/file",
+    "href": "file:///C:/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "C|\\",
+    "base": "file://host/dir/file",
+    "href": "file:///C:/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "C",
+    "base": "file://host/dir/file",
+    "href": "file://host/dir/C",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "host",
+    "hostname": "host",
+    "port": "",
+    "pathname": "/dir/C",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "C|a",
+    "base": "file://host/dir/file",
+    "href": "file://host/dir/C|a",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "host",
+    "hostname": "host",
+    "port": "",
+    "pathname": "/dir/C|a",
+    "search": "",
+    "hash": ""
+  },
+  "# Windows drive letter quirk in the file slash state",
+  {
+    "input": "/c:/foo/bar",
+    "base": "file:///c:/baz/qux",
+    "href": "file:///c:/foo/bar",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/c:/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/c|/foo/bar",
+    "base": "file:///c:/baz/qux",
+    "href": "file:///c:/foo/bar",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/c:/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:\\c:\\foo\\bar",
+    "base": "file:///c:/baz/qux",
+    "href": "file:///c:/foo/bar",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/c:/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "/c:/foo/bar",
+    "base": "file://host/path",
+    "href": "file:///c:/foo/bar",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/c:/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  "# Windows drive letter quirk with not empty host",
+  {
+    "input": "file://example.net/C:/",
+    "base": "about:blank",
+    "href": "file:///C:/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file://1.2.3.4/C:/",
+    "base": "about:blank",
+    "href": "file:///C:/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file://[1::8]/C:/",
+    "base": "about:blank",
+    "href": "file:///C:/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/",
+    "search": "",
+    "hash": ""
+  },
+  "# Windows drive letter quirk (no host)",
+  {
+    "input": "file:/C|/",
+    "base": "about:blank",
+    "href": "file:///C:/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file://C|/",
+    "base": "about:blank",
+    "href": "file:///C:/",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/C:/",
+    "search": "",
+    "hash": ""
+  },
+  "# file URLs without base URL by Rimas Misevičius",
+  {
+    "input": "file:",
+    "base": "about:blank",
+    "href": "file:///",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "file:?q=v",
+    "base": "about:blank",
+    "href": "file:///?q=v",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "?q=v",
+    "hash": ""
+  },
+  {
+    "input": "file:#frag",
+    "base": "about:blank",
+    "href": "file:///#frag",
+    "protocol": "file:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": "#frag"
+  },
+  "# IPv6 tests",
+  {
+    "input": "http://[1:0::]",
+    "base": "http://example.net/",
+    "href": "http://[1::]/",
+    "origin": "http://[1::]",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "[1::]",
+    "hostname": "[1::]",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://[0:1:2:3:4:5:6:7:8]",
+    "base": "http://example.net/",
+    "failure": true
+  },
+  {
+    "input": "https://[0::0::0]",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://[0:.0]",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://[0:0:]",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://[0:1:2:3:4:5:6:7.0.0.0.1]",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://[0:1.00.0.0.0]",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://[0:1.290.0.0.0]",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "https://[0:1.23.23]",
+    "base": "about:blank",
+    "failure": true
+  },
+  "# Empty host",
+  {
+    "input": "http://?",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "http://#",
+    "base": "about:blank",
+    "failure": true
+  },
+  "Port overflow (2^32 + 81)",
+  {
+    "input": "http://f:4294967377/c",
+    "base": "http://example.org/",
+    "failure": true
+  },
+  "Port overflow (2^64 + 81)",
+  {
+    "input": "http://f:18446744073709551697/c",
+    "base": "http://example.org/",
+    "failure": true
+  },
+  "Port overflow (2^128 + 81)",
+  {
+    "input": "http://f:340282366920938463463374607431768211537/c",
+    "base": "http://example.org/",
+    "failure": true
+  },
+  "# Non-special-URL path tests",
+  {
+    "input": "sc://ñ",
+    "base": "about:blank",
+    "href": "sc://%C3%B1",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "%C3%B1",
+    "hostname": "%C3%B1",
+    "port": "",
+    "pathname": "",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "sc://ñ?x",
+    "base": "about:blank",
+    "href": "sc://%C3%B1?x",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "%C3%B1",
+    "hostname": "%C3%B1",
+    "port": "",
+    "pathname": "",
+    "search": "?x",
+    "hash": ""
+  },
+  {
+    "input": "sc://ñ#x",
+    "base": "about:blank",
+    "href": "sc://%C3%B1#x",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "%C3%B1",
+    "hostname": "%C3%B1",
+    "port": "",
+    "pathname": "",
+    "search": "",
+    "hash": "#x"
+  },
+  {
+    "input": "#x",
+    "base": "sc://ñ",
+    "href": "sc://%C3%B1#x",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "%C3%B1",
+    "hostname": "%C3%B1",
+    "port": "",
+    "pathname": "",
+    "search": "",
+    "hash": "#x"
+  },
+  {
+    "input": "?x",
+    "base": "sc://ñ",
+    "href": "sc://%C3%B1?x",
+    "origin": "null",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "%C3%B1",
+    "hostname": "%C3%B1",
+    "port": "",
+    "pathname": "",
+    "search": "?x",
+    "hash": ""
+  },
+  {
+    "input": "sc://?",
+    "base": "about:blank",
+    "href": "sc://?",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "sc://#",
+    "base": "about:blank",
+    "href": "sc://#",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "///",
+    "base": "sc://x/",
+    "href": "sc:///",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "////",
+    "base": "sc://x/",
+    "href": "sc:////",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "//",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "////x/",
+    "base": "sc://x/",
+    "href": "sc:////x/",
+    "protocol": "sc:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "//x/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "tftp://foobar.com/someconfig;mode=netascii",
+    "base": "about:blank",
+    "href": "tftp://foobar.com/someconfig;mode=netascii",
+    "origin": "null",
+    "protocol": "tftp:",
+    "username": "",
+    "password": "",
+    "host": "foobar.com",
+    "hostname": "foobar.com",
+    "port": "",
+    "pathname": "/someconfig;mode=netascii",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "telnet://user:pass@foobar.com:23/",
+    "base": "about:blank",
+    "href": "telnet://user:pass@foobar.com:23/",
+    "origin": "null",
+    "protocol": "telnet:",
+    "username": "user",
+    "password": "pass",
+    "host": "foobar.com:23",
+    "hostname": "foobar.com",
+    "port": "23",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "ut2004://10.10.10.10:7777/Index.ut2",
+    "base": "about:blank",
+    "href": "ut2004://10.10.10.10:7777/Index.ut2",
+    "origin": "null",
+    "protocol": "ut2004:",
+    "username": "",
+    "password": "",
+    "host": "10.10.10.10:7777",
+    "hostname": "10.10.10.10",
+    "port": "7777",
+    "pathname": "/Index.ut2",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz",
+    "base": "about:blank",
+    "href": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz",
+    "origin": "null",
+    "protocol": "redis:",
+    "username": "foo",
+    "password": "bar",
+    "host": "somehost:6379",
+    "hostname": "somehost",
+    "port": "6379",
+    "pathname": "/0",
+    "search": "?baz=bam&qux=baz",
+    "hash": ""
+  },
+  {
+    "input": "rsync://foo@host:911/sup",
+    "base": "about:blank",
+    "href": "rsync://foo@host:911/sup",
+    "origin": "null",
+    "protocol": "rsync:",
+    "username": "foo",
+    "password": "",
+    "host": "host:911",
+    "hostname": "host",
+    "port": "911",
+    "pathname": "/sup",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "git://github.com/foo/bar.git",
+    "base": "about:blank",
+    "href": "git://github.com/foo/bar.git",
+    "origin": "null",
+    "protocol": "git:",
+    "username": "",
+    "password": "",
+    "host": "github.com",
+    "hostname": "github.com",
+    "port": "",
+    "pathname": "/foo/bar.git",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "irc://myserver.com:6999/channel?passwd",
+    "base": "about:blank",
+    "href": "irc://myserver.com:6999/channel?passwd",
+    "origin": "null",
+    "protocol": "irc:",
+    "username": "",
+    "password": "",
+    "host": "myserver.com:6999",
+    "hostname": "myserver.com",
+    "port": "6999",
+    "pathname": "/channel",
+    "search": "?passwd",
+    "hash": ""
+  },
+  {
+    "input": "dns://fw.example.org:9999/foo.bar.org?type=TXT",
+    "base": "about:blank",
+    "href": "dns://fw.example.org:9999/foo.bar.org?type=TXT",
+    "origin": "null",
+    "protocol": "dns:",
+    "username": "",
+    "password": "",
+    "host": "fw.example.org:9999",
+    "hostname": "fw.example.org",
+    "port": "9999",
+    "pathname": "/foo.bar.org",
+    "search": "?type=TXT",
+    "hash": ""
+  },
+  {
+    "input": "ldap://localhost:389/ou=People,o=JNDITutorial",
+    "base": "about:blank",
+    "href": "ldap://localhost:389/ou=People,o=JNDITutorial",
+    "origin": "null",
+    "protocol": "ldap:",
+    "username": "",
+    "password": "",
+    "host": "localhost:389",
+    "hostname": "localhost",
+    "port": "389",
+    "pathname": "/ou=People,o=JNDITutorial",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "git+https://github.com/foo/bar",
+    "base": "about:blank",
+    "href": "git+https://github.com/foo/bar",
+    "origin": "null",
+    "protocol": "git+https:",
+    "username": "",
+    "password": "",
+    "host": "github.com",
+    "hostname": "github.com",
+    "port": "",
+    "pathname": "/foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "urn:ietf:rfc:2648",
+    "base": "about:blank",
+    "href": "urn:ietf:rfc:2648",
+    "origin": "null",
+    "protocol": "urn:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "ietf:rfc:2648",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "tag:joe@example.org,2001:foo/bar",
+    "base": "about:blank",
+    "href": "tag:joe@example.org,2001:foo/bar",
+    "origin": "null",
+    "protocol": "tag:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "joe@example.org,2001:foo/bar",
+    "search": "",
+    "hash": ""
+  },
+  "# percent encoded hosts in non-special-URLs",
+  {
+    "input": "non-special://%E2%80%A0/",
+    "base": "about:blank",
+    "href": "non-special://%E2%80%A0/",
+    "protocol": "non-special:",
+    "username": "",
+    "password": "",
+    "host": "%E2%80%A0",
+    "hostname": "%E2%80%A0",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "non-special://H%4fSt/path",
+    "base": "about:blank",
+    "href": "non-special://H%4fSt/path",
+    "protocol": "non-special:",
+    "username": "",
+    "password": "",
+    "host": "H%4fSt",
+    "hostname": "H%4fSt",
+    "port": "",
+    "pathname": "/path",
+    "search": "",
+    "hash": ""
+  },
+  "# IPv6 in non-special-URLs",
+  {
+    "input": "non-special://[1:2:0:0:5:0:0:0]/",
+    "base": "about:blank",
+    "href": "non-special://[1:2:0:0:5::]/",
+    "protocol": "non-special:",
+    "username": "",
+    "password": "",
+    "host": "[1:2:0:0:5::]",
+    "hostname": "[1:2:0:0:5::]",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "non-special://[1:2:0:0:0:0:0:3]/",
+    "base": "about:blank",
+    "href": "non-special://[1:2::3]/",
+    "protocol": "non-special:",
+    "username": "",
+    "password": "",
+    "host": "[1:2::3]",
+    "hostname": "[1:2::3]",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "non-special://[1:2::3]:80/",
+    "base": "about:blank",
+    "href": "non-special://[1:2::3]:80/",
+    "protocol": "non-special:",
+    "username": "",
+    "password": "",
+    "host": "[1:2::3]:80",
+    "hostname": "[1:2::3]",
+    "port": "80",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "non-special://[:80/",
+    "base": "about:blank",
+    "failure": true
+  },
+  {
+    "input": "blob:https://example.com:443/",
+    "base": "about:blank",
+    "href": "blob:https://example.com:443/",
+    "protocol": "blob:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "https://example.com:443/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf",
+    "base": "about:blank",
+    "href": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf",
+    "protocol": "blob:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "d3958f5c-0777-0845-9dcf-2cb28783acaf",
+    "search": "",
+    "hash": ""
+  },
+  "Invalid IPv4 radix digits",
+  {
+    "input": "http://0177.0.0.0189",
+    "base": "about:blank",
+    "href": "http://0177.0.0.0189/",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "0177.0.0.0189",
+    "hostname": "0177.0.0.0189",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://0x7f.0.0.0x7g",
+    "base": "about:blank",
+    "href": "http://0x7f.0.0.0x7g/",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "0x7f.0.0.0x7g",
+    "hostname": "0x7f.0.0.0x7g",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://0X7F.0.0.0X7G",
+    "base": "about:blank",
+    "href": "http://0x7f.0.0.0x7g/",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "0x7f.0.0.0x7g",
+    "hostname": "0x7f.0.0.0x7g",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "Invalid IPv4 portion of IPv6 address",
+  {
+    "input": "http://[::127.0.0.0.1]",
+    "base": "about:blank",
+    "failure": true
+  },
+  "Uncompressed IPv6 addresses with 0",
+  {
+    "input": "http://[0:1:0:1:0:1:0:1]",
+    "base": "about:blank",
+    "href": "http://[0:1:0:1:0:1:0:1]/",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "[0:1:0:1:0:1:0:1]",
+    "hostname": "[0:1:0:1:0:1:0:1]",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://[1:0:1:0:1:0:1:0]",
+    "base": "about:blank",
+    "href": "http://[1:0:1:0:1:0:1:0]/",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "[1:0:1:0:1:0:1:0]",
+    "hostname": "[1:0:1:0:1:0:1:0]",
+    "port": "",
+    "pathname": "/",
+    "search": "",
+    "hash": ""
+  },
+  "Percent-encoded query and fragment",
+  {
+    "input": "http://example.org/test?\u0022",
+    "base": "about:blank",
+    "href": "http://example.org/test?%22",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/test",
+    "search": "?%22",
+    "hash": ""
+  },
+  {
+    "input": "http://example.org/test?\u0023",
+    "base": "about:blank",
+    "href": "http://example.org/test?#",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/test",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "http://example.org/test?\u003C",
+    "base": "about:blank",
+    "href": "http://example.org/test?%3C",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/test",
+    "search": "?%3C",
+    "hash": ""
+  },
+  {
+    "input": "http://example.org/test?\u003E",
+    "base": "about:blank",
+    "href": "http://example.org/test?%3E",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/test",
+    "search": "?%3E",
+    "hash": ""
+  },
+  {
+    "input": "http://example.org/test?\u2323",
+    "base": "about:blank",
+    "href": "http://example.org/test?%E2%8C%A3",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/test",
+    "search": "?%E2%8C%A3",
+    "hash": ""
+  },
+  {
+    "input": "http://example.org/test?%23%23",
+    "base": "about:blank",
+    "href": "http://example.org/test?%23%23",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/test",
+    "search": "?%23%23",
+    "hash": ""
+  },
+  {
+    "input": "http://example.org/test?%GH",
+    "base": "about:blank",
+    "href": "http://example.org/test?%GH",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/test",
+    "search": "?%GH",
+    "hash": ""
+  },
+  {
+    "input": "http://example.org/test?a#%EF",
+    "base": "about:blank",
+    "href": "http://example.org/test?a#%EF",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/test",
+    "search": "?a",
+    "hash": "#%EF"
+  },
+  {
+    "input": "http://example.org/test?a#%GH",
+    "base": "about:blank",
+    "href": "http://example.org/test?a#%GH",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/test",
+    "search": "?a",
+    "hash": "#%GH"
+  },
+  "Bad bases",
+  {
+    "input": "test-a.html",
+    "base": "a",
+    "failure": true
+  },
+  {
+    "input": "test-a-slash.html",
+    "base": "a/",
+    "failure": true
+  },
+  {
+    "input": "test-a-slash-slash.html",
+    "base": "a//",
+    "failure": true
+  },
+  {
+    "input": "test-a-colon.html",
+    "base": "a:",
+    "failure": true
+  },
+  {
+    "input": "test-a-colon-slash.html",
+    "base": "a:/",
+    "href": "a:/test-a-colon-slash.html",
+    "protocol": "a:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/test-a-colon-slash.html",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "test-a-colon-slash-slash.html",
+    "base": "a://",
+    "href": "a:///test-a-colon-slash-slash.html",
+    "protocol": "a:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/test-a-colon-slash-slash.html",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "test-a-colon-b.html",
+    "base": "a:b",
+    "failure": true
+  },
+  {
+    "input": "test-a-colon-slash-b.html",
+    "base": "a:/b",
+    "href": "a:/test-a-colon-slash-b.html",
+    "protocol": "a:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "/test-a-colon-slash-b.html",
+    "search": "",
+    "hash": ""
+  },
+  {
+    "input": "test-a-colon-slash-slash-b.html",
+    "base": "a://b",
+    "href": "a://b/test-a-colon-slash-slash-b.html",
+    "protocol": "a:",
+    "username": "",
+    "password": "",
+    "host": "b",
+    "hostname": "b",
+    "port": "",
+    "pathname": "/test-a-colon-slash-slash-b.html",
+    "search": "",
+    "hash": ""
+  },
+  "Null code point in fragment",
+  {
+    "input": "http://example.org/test?a#b\u0000c",
+    "base": "about:blank",
+    "href": "http://example.org/test?a#bc",
+    "protocol": "http:",
+    "username": "",
+    "password": "",
+    "host": "example.org",
+    "hostname": "example.org",
+    "port": "",
+    "pathname": "/test",
+    "search": "?a",
+    "hash": "#bc"
+  }
+]
diff --git a/test/fixtures/url-toascii.js b/test/fixtures/url-toascii.js
new file mode 100644
index 00000000000000..59b76330f867f2
--- /dev/null
+++ b/test/fixtures/url-toascii.js
@@ -0,0 +1,157 @@
+'use strict';
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/4839a0a804/url/toascii.json
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+module.exports =
+[
+  "This resource is focused on highlighting issues with UTS #46 ToASCII",
+  {
+    "comment": "Label with hyphens in 3rd and 4th position",
+    "input": "aa--",
+    "output": "aa--"
+  },
+  {
+    "input": "a†--",
+    "output": "xn--a---kp0a"
+  },
+  {
+    "input": "ab--c",
+    "output": "ab--c"
+  },
+  {
+    "comment": "Label with leading hyphen",
+    "input": "-x",
+    "output": "-x"
+  },
+  {
+    "input": "-†",
+    "output": "xn----xhn"
+  },
+  {
+    "input": "-x.xn--nxa",
+    "output": "-x.xn--nxa"
+  },
+  {
+    "input": "-x.β",
+    "output": "-x.xn--nxa"
+  },
+  {
+    "comment": "Label with trailing hyphen",
+    "input": "x-.xn--nxa",
+    "output": "x-.xn--nxa"
+  },
+  {
+    "input": "x-.β",
+    "output": "x-.xn--nxa"
+  },
+  {
+    "comment": "Empty labels",
+    "input": "x..xn--nxa",
+    "output": "x..xn--nxa"
+  },
+  {
+    "input": "x..β",
+    "output": "x..xn--nxa"
+  },
+  {
+    "comment": "Invalid Punycode",
+    "input": "xn--a",
+    "output": null
+  },
+  {
+    "input": "xn--a.xn--nxa",
+    "output": null
+  },
+  {
+    "input": "xn--a.β",
+    "output": null
+  },
+  {
+    "comment": "Valid Punycode",
+    "input": "xn--nxa.xn--nxa",
+    "output": "xn--nxa.xn--nxa"
+  },
+  {
+    "comment": "Mixed",
+    "input": "xn--nxa.β",
+    "output": "xn--nxa.xn--nxa"
+  },
+  {
+    "input": "ab--c.xn--nxa",
+    "output": "ab--c.xn--nxa"
+  },
+  {
+    "input": "ab--c.β",
+    "output": "ab--c.xn--nxa"
+  },
+  {
+    "comment": "CheckJoiners is true",
+    "input": "\u200D.example",
+    "output": null
+  },
+  {
+    "input": "xn--1ug.example",
+    "output": null
+  },
+  {
+    "comment": "CheckBidi is true",
+    "input": "يa",
+    "output": null
+  },
+  {
+    "input": "xn--a-yoc",
+    "output": null
+  },
+  {
+    "comment": "processing_option is Nontransitional_Processing",
+    "input": "ශ්‍රී",
+    "output": "xn--10cl1a0b660p"
+  },
+  {
+    "input": "نامه‌ای",
+    "output": "xn--mgba3gch31f060k"
+  },
+  {
+    "comment": "U+FFFD",
+    "input": "\uFFFD.com",
+    "output": null
+  },
+  {
+    "comment": "U+FFFD character encoded in Punycode",
+    "input": "xn--zn7c.com",
+    "output": null
+  },
+  {
+    "comment": "Label longer than 63 code points",
+    "input": "x01234567890123456789012345678901234567890123456789012345678901x",
+    "output": "x01234567890123456789012345678901234567890123456789012345678901x"
+  },
+  {
+    "input": "x01234567890123456789012345678901234567890123456789012345678901†",
+    "output": "xn--x01234567890123456789012345678901234567890123456789012345678901-6963b"
+  },
+  {
+    "input": "x01234567890123456789012345678901234567890123456789012345678901x.xn--nxa",
+    "output": "x01234567890123456789012345678901234567890123456789012345678901x.xn--nxa"
+  },
+  {
+    "input": "x01234567890123456789012345678901234567890123456789012345678901x.β",
+    "output": "x01234567890123456789012345678901234567890123456789012345678901x.xn--nxa"
+  },
+  {
+    "comment": "Domain excluding TLD longer than 253 code points",
+    "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.x",
+    "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.x"
+  },
+  {
+    "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--nxa",
+    "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--nxa"
+  },
+  {
+    "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.β",
+    "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--nxa"
+  }
+]
diff --git a/test/parallel/test-icu-punycode.js b/test/parallel/test-icu-punycode.js
index cf83ac66206806..7abcae461774c0 100644
--- a/test/parallel/test-icu-punycode.js
+++ b/test/parallel/test-icu-punycode.js
@@ -1,70 +1,46 @@
 'use strict';
-
 const common = require('../common');
-const icu = getPunycode();
+
+if (!common.hasIntl)
+  common.skip('missing Intl');
+
+const icu = process.binding('icu');
 const assert = require('assert');
 
-function getPunycode() {
-  try {
-    return process.binding('icu');
-  } catch (err) {
-    return undefined;
+const tests = require('../fixtures/url-idna.js');
+const wptToASCIITests = require('../fixtures/url-toascii.js');
+
+{
+  for (const [i, { ascii, unicode }] of tests.entries()) {
+    assert.strictEqual(ascii, icu.toASCII(unicode), `toASCII(${i + 1})`);
+    assert.strictEqual(unicode, icu.toUnicode(ascii), `toUnicode(${i + 1})`);
+    assert.strictEqual(ascii, icu.toASCII(icu.toUnicode(ascii)),
+                       `toASCII(toUnicode(${i + 1}))`);
+    assert.strictEqual(unicode, icu.toUnicode(icu.toASCII(unicode)),
+                       `toUnicode(toASCII(${i + 1}))`);
   }
 }
 
-if (!icu)
-  common.skip('icu punycode tests because ICU is not present.');
+{
+  const errMessage = /^Error: Cannot convert name to ASCII$/;
 
-// Credit for list: http://www.i18nguy.com/markup/idna-examples.html
-const tests = [
-  'افغانستا.icom.museum',
-  'الجزائر.icom.museum',
-  'österreich.icom.museum',
-  'বাংলাদেশ.icom.museum',
-  'беларусь.icom.museum',
-  'belgië.icom.museum',
-  'българия.icom.museum',
-  'تشادر.icom.museum',
-  '中国.icom.museum',
-  'القمر.icom.museum',
-  'κυπρος.icom.museum',
-  'českárepublika.icom.museum',
-  'مصر.icom.museum',
-  'ελλάδα.icom.museum',
-  'magyarország.icom.museum',
-  'ísland.icom.museum',
-  'भारत.icom.museum',
-  'ايران.icom.museum',
-  'éire.icom.museum',
-  'איקו״ם.ישראל.museum',
-  '日本.icom.museum',
-  'الأردن.icom.museum',
-  'қазақстан.icom.museum',
-  '한국.icom.museum',
-  'кыргызстан.icom.museum',
-  'ລາວ.icom.museum',
-  'لبنان.icom.museum',
-  'македонија.icom.museum',
-  'موريتانيا.icom.museum',
-  'méxico.icom.museum',
-  'монголулс.icom.museum',
-  'المغرب.icom.museum',
-  'नेपाल.icom.museum',
-  'عمان.icom.museum',
-  'قطر.icom.museum',
-  'românia.icom.museum',
-  'россия.иком.museum',
-  'србијаицрнагора.иком.museum',
-  'இலங்கை.icom.museum',
-  'españa.icom.museum',
-  'ไทย.icom.museum',
-  'تونس.icom.museum',
-  'türkiye.icom.museum',
-  'украина.icom.museum',
-  'việtnam.icom.museum'
-];
-
-// Testing the roundtrip
-tests.forEach((i) => {
-  assert.strictEqual(i, icu.toUnicode(icu.toASCII(i)));
-});
+  for (const [i, test] of wptToASCIITests.entries()) {
+    if (typeof test === 'string')
+      continue; // skip comments
+    const { comment, input, output } = test;
+    let caseComment = `case ${i + 1}`;
+    if (comment)
+      caseComment += ` (${comment})`;
+    if (output === null) {
+      assert.throws(() => icu.toASCII(input),
+                    errMessage, `ToASCII ${caseComment}`);
+      assert.doesNotThrow(() => icu.toASCII(input, true),
+                          `ToASCII ${caseComment} in lenient mode`);
+    } else {
+      assert.strictEqual(icu.toASCII(input), output, `ToASCII ${caseComment}`);
+      assert.strictEqual(icu.toASCII(input, true), output,
+                         `ToASCII ${caseComment} in lenient mode`);
+    }
+    assert.doesNotThrow(() => icu.toUnicode(input), `ToUnicode ${caseComment}`);
+  }
+}
diff --git a/test/parallel/test-intl-v8BreakIterator.js b/test/parallel/test-intl-v8BreakIterator.js
index 03e0712ba3ece2..7ccdb70f141a8b 100644
--- a/test/parallel/test-intl-v8BreakIterator.js
+++ b/test/parallel/test-intl-v8BreakIterator.js
@@ -2,8 +2,9 @@
 const common = require('../common');
 const assert = require('assert');
 
-if (global.Intl === undefined || Intl.v8BreakIterator === undefined)
+if (!common.hasIntl || Intl.v8BreakIterator === undefined) {
   common.skip('no Intl');
+}
 
 try {
   new Intl.v8BreakIterator();
diff --git a/test/parallel/test-intl.js b/test/parallel/test-intl.js
index a5b76538a2e722..907f56f03ac83f 100644
--- a/test/parallel/test-intl.js
+++ b/test/parallel/test-intl.js
@@ -8,9 +8,6 @@ if (enablei18n === undefined) {
   enablei18n = 0;
 }
 
-// is the Intl object present?
-const haveIntl = (global.Intl !== undefined);
-
 // Returns true if no specific locale ids were configured (i.e. "all")
 // Else, returns true if loc is in the configured list
 // Else, returns false
@@ -19,7 +16,7 @@ function haveLocale(loc) {
   return locs.includes(loc);
 }
 
-if (!haveIntl) {
+if (!common.hasIntl) {
   const erMsg =
     `"Intl" object is NOT present but v8_enable_i18n_support is ${enablei18n}`;
   assert.strictEqual(enablei18n, 0, erMsg);
diff --git a/test/parallel/test-process-versions.js b/test/parallel/test-process-versions.js
index 27311f7eda84e3..b0377afd4a1899 100644
--- a/test/parallel/test-process-versions.js
+++ b/test/parallel/test-process-versions.js
@@ -9,7 +9,7 @@ if (common.hasCrypto) {
   expected_keys.push('openssl');
 }
 
-if (typeof Intl !== 'undefined') {
+if (common.hasIntl) {
   expected_keys.push('icu');
 }
 
diff --git a/test/parallel/test-repl-tab-complete.js b/test/parallel/test-repl-tab-complete.js
index df7300e526e8c1..c37d4bc650a508 100644
--- a/test/parallel/test-repl-tab-complete.js
+++ b/test/parallel/test-repl-tab-complete.js
@@ -294,7 +294,7 @@ const testNonGlobal = repl.start({
 const builtins = [['Infinity', '', 'Int16Array', 'Int32Array',
                    'Int8Array'], 'I'];
 
-if (typeof Intl === 'object') {
+if (common.hasIntl) {
   builtins[0].push('Intl');
 }
 testNonGlobal.complete('I', common.mustCall((error, data) => {
diff --git a/test/parallel/test-url-domain-ascii-unicode.js b/test/parallel/test-url-domain-ascii-unicode.js
new file mode 100644
index 00000000000000..49259a7ab0f4c4
--- /dev/null
+++ b/test/parallel/test-url-domain-ascii-unicode.js
@@ -0,0 +1,31 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasIntl)
+  common.skip('missing Intl');
+
+const strictEqual = require('assert').strictEqual;
+const url = require('url');
+
+const domainToASCII = url.domainToASCII;
+const domainToUnicode = url.domainToUnicode;
+
+const domainWithASCII = [
+  ['ıíd', 'xn--d-iga7r'],
+  ['يٴ', 'xn--mhb8f'],
+  ['www.ϧƽəʐ.com', 'www.xn--cja62apfr6c.com'],
+  ['новини.com', 'xn--b1amarcd.com'],
+  ['名がドメイン.com', 'xn--v8jxj3d1dzdz08w.com'],
+  ['افغانستا.icom.museum', 'xn--mgbaal8b0b9b2b.icom.museum'],
+  ['الجزائر.icom.fake', 'xn--lgbbat1ad8j.icom.fake'],
+  ['भारत.org', 'xn--h2brj9c.org']
+];
+
+domainWithASCII.forEach((pair) => {
+  const domain = pair[0];
+  const ascii = pair[1];
+  const domainConvertedToASCII = domainToASCII(domain);
+  strictEqual(domainConvertedToASCII, ascii);
+  const asciiConvertedToUnicode = domainToUnicode(ascii);
+  strictEqual(asciiConvertedToUnicode, domain);
+});
diff --git a/test/parallel/test-whatwg-url-constructor.js b/test/parallel/test-whatwg-url-constructor.js
new file mode 100644
index 00000000000000..16bcac74cc6bcd
--- /dev/null
+++ b/test/parallel/test-whatwg-url-constructor.js
@@ -0,0 +1,143 @@
+'use strict';
+const common = require('../common');
+if (!common.hasIntl) {
+  // A handful of the tests fail when ICU is not included.
+  common.skip('missing Intl');
+}
+
+const fixtures = require('../common/fixtures');
+const { URL, URLSearchParams } = require('url');
+const { test, assert_equals, assert_true, assert_throws } =
+  require('../common/wpt');
+
+const request = {
+  response: require(fixtures.path('url-tests'))
+};
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/8791bed/url/url-constructor.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+function runURLConstructorTests() {
+  // var setup = async_test("Loading data…")
+  // setup.step(function() {
+  //   var request = new XMLHttpRequest()
+  //   request.open("GET", "urltestdata.json")
+  //   request.send()
+  //   request.responseType = "json"
+  //   request.onload = setup.step_func(function() {
+         runURLTests(request.response)
+  //     setup.done()
+  //   })
+  // })
+}
+
+function bURL(url, base) {
+  return new URL(url, base || "about:blank")
+}
+
+
+function runURLTests(urltests) {
+  for(var i = 0, l = urltests.length; i < l; i++) {
+    var expected = urltests[i]
+    if (typeof expected === "string") continue // skip comments
+
+    test(function() {
+      if (expected.failure) {
+        assert_throws(new TypeError(), function() {
+          bURL(expected.input, expected.base)
+        })
+        return
+      }
+
+      var url = bURL(expected.input, expected.base)
+      assert_equals(url.href, expected.href, "href")
+      assert_equals(url.protocol, expected.protocol, "protocol")
+      assert_equals(url.username, expected.username, "username")
+      assert_equals(url.password, expected.password, "password")
+      assert_equals(url.host, expected.host, "host")
+      assert_equals(url.hostname, expected.hostname, "hostname")
+      assert_equals(url.port, expected.port, "port")
+      assert_equals(url.pathname, expected.pathname, "pathname")
+      assert_equals(url.search, expected.search, "search")
+      if ("searchParams" in expected) {
+        assert_true("searchParams" in url)
+        assert_equals(url.searchParams.toString(), expected.searchParams, "searchParams")
+      }
+      assert_equals(url.hash, expected.hash, "hash")
+    }, "Parsing: <" + expected.input + "> against <" + expected.base + ">")
+  }
+}
+
+function runURLSearchParamTests() {
+  test(function() {
+    var url = bURL('http://example.org/?a=b')
+    assert_true("searchParams" in url)
+    var searchParams = url.searchParams
+    assert_true(url.searchParams === searchParams, 'Object identity should hold.')
+  }, 'URL.searchParams getter')
+
+  test(function() {
+    var url = bURL('http://example.org/?a=b')
+    assert_true("searchParams" in url)
+    var searchParams = url.searchParams
+    assert_equals(searchParams.toString(), 'a=b')
+
+    searchParams.set('a', 'b')
+    assert_equals(url.searchParams.toString(), 'a=b')
+    assert_equals(url.search, '?a=b')
+    url.search = ''
+    assert_equals(url.searchParams.toString(), '')
+    assert_equals(url.search, '')
+    assert_equals(searchParams.toString(), '')
+  }, 'URL.searchParams updating, clearing')
+
+  test(function() {
+    'use strict'
+    var urlString = 'http://example.org'
+    var url = bURL(urlString)
+    assert_throws(TypeError(), function() { url.searchParams = new URLSearchParams(urlString) })
+  }, 'URL.searchParams setter, invalid values')
+
+  test(function() {
+    var url = bURL('http://example.org/file?a=b&c=d')
+    assert_true("searchParams" in url)
+    var searchParams = url.searchParams
+    assert_equals(url.search, '?a=b&c=d')
+    assert_equals(searchParams.toString(), 'a=b&c=d')
+
+    // Test that setting 'search' propagates to the URL object's query object.
+    url.search = 'e=f&g=h'
+    assert_equals(url.search, '?e=f&g=h')
+    assert_equals(searchParams.toString(), 'e=f&g=h')
+
+    // ..and same but with a leading '?'.
+    url.search = '?e=f&g=h'
+    assert_equals(url.search, '?e=f&g=h')
+    assert_equals(searchParams.toString(), 'e=f&g=h')
+
+    // And in the other direction, altering searchParams propagates
+    // back to 'search'.
+    searchParams.append('i', ' j ')
+    assert_equals(url.search, '?e=f&g=h&i=+j+')
+    assert_equals(url.searchParams.toString(), 'e=f&g=h&i=+j+')
+    assert_equals(searchParams.get('i'), ' j ')
+
+    searchParams.set('e', 'updated')
+    assert_equals(url.search, '?e=updated&g=h&i=+j+')
+    assert_equals(searchParams.get('e'), 'updated')
+
+    var url2 = bURL('http://example.org/file??a=b&c=d')
+    assert_equals(url2.search, '??a=b&c=d')
+    assert_equals(url2.searchParams.toString(), '%3Fa=b&c=d')
+
+    url2.href = 'http://example.org/file??a=b'
+    assert_equals(url2.search, '??a=b')
+    assert_equals(url2.searchParams.toString(), '%3Fa=b')
+  }, 'URL.searchParams and URL.search setters, update propagation')
+}
+runURLSearchParamTests()
+runURLConstructorTests()
+/* eslint-enable */
diff --git a/test/parallel/test-whatwg-url-domainto.js b/test/parallel/test-whatwg-url-domainto.js
new file mode 100644
index 00000000000000..f8029d8b5d66a4
--- /dev/null
+++ b/test/parallel/test-whatwg-url-domainto.js
@@ -0,0 +1,51 @@
+'use strict';
+const common = require('../common');
+
+if (!common.hasIntl)
+  common.skip('missing Intl');
+
+const assert = require('assert');
+const { domainToASCII, domainToUnicode } = require('url');
+
+// Tests below are not from WPT.
+const tests = require('../fixtures/url-idna.js');
+const wptToASCIITests = require('../fixtures/url-toascii.js');
+
+{
+  assert.throws(() => domainToASCII(), /^TypeError: The "domain" argument must be specified$/);
+  assert.throws(() => domainToUnicode(), /^TypeError: The "domain" argument must be specified$/);
+  assert.strictEqual(domainToASCII(undefined), 'undefined');
+  assert.strictEqual(domainToUnicode(undefined), 'undefined');
+}
+
+{
+  for (const [i, { ascii, unicode }] of tests.entries()) {
+    assert.strictEqual(ascii, domainToASCII(unicode),
+                       `domainToASCII(${i + 1})`);
+    assert.strictEqual(unicode, domainToUnicode(ascii),
+                       `domainToUnicode(${i + 1})`);
+    assert.strictEqual(ascii, domainToASCII(domainToUnicode(ascii)),
+                       `domainToASCII(domainToUnicode(${i + 1}))`);
+    assert.strictEqual(unicode, domainToUnicode(domainToASCII(unicode)),
+                       `domainToUnicode(domainToASCII(${i + 1}))`);
+  }
+}
+
+{
+  for (const [i, test] of wptToASCIITests.entries()) {
+    if (typeof test === 'string')
+      continue; // skip comments
+    const { comment, input, output } = test;
+    let caseComment = `Case ${i + 1}`;
+    if (comment)
+      caseComment += ` (${comment})`;
+    if (output === null) {
+      assert.strictEqual(domainToASCII(input), '', caseComment);
+      assert.strictEqual(domainToUnicode(input), '', caseComment);
+    } else {
+      assert.strictEqual(domainToASCII(input), output, caseComment);
+      const roundtripped = domainToASCII(domainToUnicode(input));
+      assert.strictEqual(roundtripped, output, caseComment);
+    }
+  }
+}
diff --git a/test/parallel/test-whatwg-url-historical.js b/test/parallel/test-whatwg-url-historical.js
new file mode 100644
index 00000000000000..466949cd322d37
--- /dev/null
+++ b/test/parallel/test-whatwg-url-historical.js
@@ -0,0 +1,46 @@
+'use strict';
+const common = require('../common');
+if (!common.hasIntl) {
+  // A handful of the tests fail when ICU is not included.
+  common.skip('missing Intl');
+}
+
+const URL = require('url').URL;
+const { test, assert_equals, assert_throws } = require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/8791bed/url/historical.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+// var objects = [
+//   [function() { return window.location }, "location object"],
+//   [function() { return document.createElement("a") }, "a element"],
+//   [function() { return document.createElement("area") }, "area element"],
+// ];
+
+// objects.forEach(function(o) {
+//   test(function() {
+//     var object = o[0]();
+//     assert_false("searchParams" in object,
+//                  o[1] + " should not have a searchParams attribute");
+//   }, "searchParams on " + o[1]);
+// });
+
+test(function() {
+  var url = new URL("./foo", "http://www.example.org");
+  assert_equals(url.href, "http://www.example.org/foo");
+  assert_throws(new TypeError(), function() {
+    url.href = "./bar";
+  });
+}, "Setting URL's href attribute and base URLs");
+
+test(function() {
+  assert_equals(URL.domainToASCII, undefined);
+}, "URL.domainToASCII should be undefined");
+
+test(function() {
+  assert_equals(URL.domainToUnicode, undefined);
+}, "URL.domainToUnicode should be undefined");
+/* eslint-enable */
diff --git a/test/parallel/test-whatwg-url-inspect.js b/test/parallel/test-whatwg-url-inspect.js
new file mode 100644
index 00000000000000..5758b39b8af83d
--- /dev/null
+++ b/test/parallel/test-whatwg-url-inspect.js
@@ -0,0 +1,66 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasIntl) {
+  // A handful of the tests fail when ICU is not included.
+  common.skip('missing Intl');
+}
+
+const util = require('util');
+const URL = require('url').URL;
+const assert = require('assert');
+
+// Tests below are not from WPT.
+const url = new URL('https://username:password@host.name:8080/path/name/?que=ry#hash');
+
+assert.strictEqual(
+  util.inspect(url),
+  `URL {
+  href: 'https://username:password@host.name:8080/path/name/?que=ry#hash',
+  origin: 'https://host.name:8080',
+  protocol: 'https:',
+  username: 'username',
+  password: 'password',
+  host: 'host.name:8080',
+  hostname: 'host.name',
+  port: '8080',
+  pathname: '/path/name/',
+  search: '?que=ry',
+  searchParams: URLSearchParams { 'que' => 'ry' },
+  hash: '#hash' }`);
+
+assert.strictEqual(
+  util.inspect(url, { showHidden: true }),
+  `URL {
+  href: 'https://username:password@host.name:8080/path/name/?que=ry#hash',
+  origin: 'https://host.name:8080',
+  protocol: 'https:',
+  username: 'username',
+  password: 'password',
+  host: 'host.name:8080',
+  hostname: 'host.name',
+  port: '8080',
+  pathname: '/path/name/',
+  search: '?que=ry',
+  searchParams: URLSearchParams { 'que' => 'ry' },
+  hash: '#hash',
+  cannotBeBase: false,
+  special: true,
+  [Symbol(context)]:\x20
+   URLContext {
+     flags: 2032,
+     scheme: 'https:',
+     username: 'username',
+     password: 'password',
+     host: 'host.name',
+     port: 8080,
+     path: [ 'path', 'name', '', [length]: 3 ],
+     query: 'que=ry',
+     fragment: 'hash' } }`);
+
+assert.strictEqual(
+  util.inspect({ a: url }, { depth: 0 }),
+  '{ a: [Object] }');
+
+class MyURL extends URL {}
+assert(util.inspect(new MyURL(url.href)).startsWith('MyURL {'));
diff --git a/test/parallel/test-whatwg-url-origin.js b/test/parallel/test-whatwg-url-origin.js
new file mode 100644
index 00000000000000..8a5ad6d3154f8f
--- /dev/null
+++ b/test/parallel/test-whatwg-url-origin.js
@@ -0,0 +1,52 @@
+'use strict';
+const common = require('../common');
+if (!common.hasIntl) {
+  // A handful of the tests fail when ICU is not included.
+  common.skip('missing Intl');
+}
+
+const fixtures = require('../common/fixtures');
+const URL = require('url').URL;
+const { test, assert_equals } = require('../common/wpt');
+
+const request = {
+  response: require(fixtures.path('url-tests'))
+};
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/8791bed/url/url-origin.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+function runURLOriginTests() {
+  // var setup = async_test("Loading data…")
+  // setup.step(function() {
+  //   var request = new XMLHttpRequest()
+  //   request.open("GET", "urltestdata.json")
+  //   request.send()
+  //   request.responseType = "json"
+  //   request.onload = setup.step_func(function() {
+         runURLTests(request.response)
+  //     setup.done()
+  //   })
+  // })
+}
+
+function bURL(url, base) {
+  return new URL(url, base || "about:blank")
+}
+
+function runURLTests(urltests) {
+  for(var i = 0, l = urltests.length; i < l; i++) {
+    var expected = urltests[i]
+    if (typeof expected === "string" || !("origin" in expected)) continue
+    test(function() {
+      var url = bURL(expected.input, expected.base)
+      assert_equals(url.origin, expected.origin, "origin")
+    }, "Origin parsing: <" + expected.input + "> against <" + expected.base + ">")
+  }
+}
+
+runURLOriginTests()
+/* eslint-enable */
diff --git a/test/parallel/test-whatwg-url-parsing.js b/test/parallel/test-whatwg-url-parsing.js
new file mode 100644
index 00000000000000..104f25ff4380bb
--- /dev/null
+++ b/test/parallel/test-whatwg-url-parsing.js
@@ -0,0 +1,62 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasIntl) {
+  // A handful of the tests fail when ICU is not included.
+  common.skip('missing Intl');
+}
+
+const URL = require('url').URL;
+const assert = require('assert');
+const fixtures = require('../common/fixtures');
+
+// Tests below are not from WPT.
+const tests = require(fixtures.path('url-tests'));
+const failureTests = tests.filter((test) => test.failure).concat([
+  { input: '' },
+  { input: 'test' },
+  { input: undefined },
+  { input: 0 },
+  { input: true },
+  { input: false },
+  { input: null },
+  { input: new Date() },
+  { input: new RegExp() },
+  { input: () => {} }
+]);
+
+const expectedError = (err) => /^TypeError: Invalid URL: /.test(err.toString());
+
+for (const test of failureTests) {
+  assert.throws(
+    () => new URL(test.input, test.base),
+    (error) => {
+      if (!expectedError(error))
+        return false;
+
+      // The input could be processed, so we don't do strict matching here
+      const match = (`${error}`).match(/Invalid URL: (.*)$/);
+      if (!match) {
+        return false;
+      }
+      return error.input === match[1];
+    });
+}
+
+const additional_tests =
+  require(fixtures.path('url-tests-additional.js'));
+
+for (const test of additional_tests) {
+  const url = new URL(test.url);
+  if (test.href) assert.strictEqual(url.href, test.href);
+  if (test.origin) assert.strictEqual(url.origin, test.origin);
+  if (test.protocol) assert.strictEqual(url.protocol, test.protocol);
+  if (test.username) assert.strictEqual(url.username, test.username);
+  if (test.password) assert.strictEqual(url.password, test.password);
+  if (test.hostname) assert.strictEqual(url.hostname, test.hostname);
+  if (test.host) assert.strictEqual(url.host, test.host);
+  if (test.port !== undefined) assert.strictEqual(url.port, test.port);
+  if (test.pathname) assert.strictEqual(url.pathname, test.pathname);
+  if (test.search) assert.strictEqual(url.search, test.search);
+  if (test.hash) assert.strictEqual(url.hash, test.hash);
+}
diff --git a/test/parallel/test-whatwg-url-properties.js b/test/parallel/test-whatwg-url-properties.js
new file mode 100644
index 00000000000000..d6caae511aed47
--- /dev/null
+++ b/test/parallel/test-whatwg-url-properties.js
@@ -0,0 +1,162 @@
+// Flags: --expose-internals
+'use strict';
+
+require('../common');
+const URL = require('url').URL;
+const assert = require('assert');
+const urlToOptions = require('internal/url').urlToOptions;
+
+// Tests below are not from WPT.
+const url = new URL('http://user:pass@foo.bar.com:21/aaa/zzz?l=24#test');
+const oldParams = url.searchParams;  // for test of [SameObject]
+
+// To retrieve enumerable but not necessarily own properties,
+// we need to use the for-in loop.
+const props = [];
+for (const prop in url) {
+  props.push(prop);
+}
+
+// See: https://url.spec.whatwg.org/#api
+// https://heycam.github.io/webidl/#es-attributes
+// https://heycam.github.io/webidl/#es-stringifier
+const expected = ['toString',
+                  'href', 'origin', 'protocol',
+                  'username', 'password', 'host', 'hostname', 'port',
+                  'pathname', 'search', 'searchParams', 'hash', 'toJSON'];
+
+assert.deepStrictEqual(props, expected);
+
+// href is writable (not readonly) and is stringifier
+assert.strictEqual(url.toString(), url.href);
+url.href = 'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test';
+assert.strictEqual(url.href,
+                   'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test');
+assert.strictEqual(url.toString(), url.href);
+// Return true because it's configurable, but because the properties
+// are defined on the prototype per the spec, the deletion has no effect
+assert.strictEqual((delete url.href), true);
+assert.strictEqual(url.href,
+                   'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test');
+assert.strictEqual(url.searchParams, oldParams);  // [SameObject]
+
+// searchParams is readonly. Under strict mode setting a
+// non-writable property should throw.
+// Note: this error message is subject to change in V8 updates
+assert.throws(
+  () => url.origin = 'http://foo.bar.com:22',
+  /^TypeError: Cannot set property origin of \[object URL\] which has only a getter$/
+);
+assert.strictEqual(url.origin, 'http://foo.bar.com:21');
+assert.strictEqual(url.toString(),
+                   'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test');
+assert.strictEqual((delete url.origin), true);
+assert.strictEqual(url.origin, 'http://foo.bar.com:21');
+
+// The following properties should be writable (not readonly)
+url.protocol = 'https:';
+assert.strictEqual(url.protocol, 'https:');
+assert.strictEqual(url.toString(),
+                   'https://user:pass@foo.bar.com:21/aaa/zzz?l=25#test');
+assert.strictEqual((delete url.protocol), true);
+assert.strictEqual(url.protocol, 'https:');
+
+url.username = 'user2';
+assert.strictEqual(url.username, 'user2');
+assert.strictEqual(url.toString(),
+                   'https://user2:pass@foo.bar.com:21/aaa/zzz?l=25#test');
+assert.strictEqual((delete url.username), true);
+assert.strictEqual(url.username, 'user2');
+
+url.password = 'pass2';
+assert.strictEqual(url.password, 'pass2');
+assert.strictEqual(url.toString(),
+                   'https://user2:pass2@foo.bar.com:21/aaa/zzz?l=25#test');
+assert.strictEqual((delete url.password), true);
+assert.strictEqual(url.password, 'pass2');
+
+url.host = 'foo.bar.net:22';
+assert.strictEqual(url.host, 'foo.bar.net:22');
+assert.strictEqual(url.toString(),
+                   'https://user2:pass2@foo.bar.net:22/aaa/zzz?l=25#test');
+assert.strictEqual((delete url.host), true);
+assert.strictEqual(url.host, 'foo.bar.net:22');
+
+url.hostname = 'foo.bar.org';
+assert.strictEqual(url.hostname, 'foo.bar.org');
+assert.strictEqual(url.toString(),
+                   'https://user2:pass2@foo.bar.org:22/aaa/zzz?l=25#test');
+assert.strictEqual((delete url.hostname), true);
+assert.strictEqual(url.hostname, 'foo.bar.org');
+
+url.port = '23';
+assert.strictEqual(url.port, '23');
+assert.strictEqual(url.toString(),
+                   'https://user2:pass2@foo.bar.org:23/aaa/zzz?l=25#test');
+assert.strictEqual((delete url.port), true);
+assert.strictEqual(url.port, '23');
+
+url.pathname = '/aaa/bbb';
+assert.strictEqual(url.pathname, '/aaa/bbb');
+assert.strictEqual(url.toString(),
+                   'https://user2:pass2@foo.bar.org:23/aaa/bbb?l=25#test');
+assert.strictEqual((delete url.pathname), true);
+assert.strictEqual(url.pathname, '/aaa/bbb');
+
+url.search = '?k=99';
+assert.strictEqual(url.search, '?k=99');
+assert.strictEqual(url.toString(),
+                   'https://user2:pass2@foo.bar.org:23/aaa/bbb?k=99#test');
+assert.strictEqual((delete url.search), true);
+assert.strictEqual(url.search, '?k=99');
+
+url.hash = '#abcd';
+assert.strictEqual(url.hash, '#abcd');
+assert.strictEqual(url.toString(),
+                   'https://user2:pass2@foo.bar.org:23/aaa/bbb?k=99#abcd');
+assert.strictEqual((delete url.hash), true);
+assert.strictEqual(url.hash, '#abcd');
+
+// searchParams is readonly. Under strict mode setting a
+// non-writable property should throw.
+// Note: this error message is subject to change in V8 updates
+assert.throws(
+  () => url.searchParams = '?k=88',
+  /^TypeError: Cannot set property searchParams of \[object URL\] which has only a getter$/
+);
+assert.strictEqual(url.searchParams, oldParams);
+assert.strictEqual(url.toString(),
+                   'https://user2:pass2@foo.bar.org:23/aaa/bbb?k=99#abcd');
+assert.strictEqual((delete url.searchParams), true);
+assert.strictEqual(url.searchParams, oldParams);
+
+// Test urlToOptions
+{
+  const opts =
+    urlToOptions(new URL('http://user:pass@foo.bar.com:21/aaa/zzz?l=24#test'));
+  assert.strictEqual(opts instanceof URL, false);
+  assert.strictEqual(opts.protocol, 'http:');
+  assert.strictEqual(opts.auth, 'user:pass');
+  assert.strictEqual(opts.hostname, 'foo.bar.com');
+  assert.strictEqual(opts.port, 21);
+  assert.strictEqual(opts.path, '/aaa/zzz?l=24');
+  assert.strictEqual(opts.pathname, '/aaa/zzz');
+  assert.strictEqual(opts.search, '?l=24');
+  assert.strictEqual(opts.hash, '#test');
+}
+
+// Test special origins
+[
+  { expected: 'https://whatwg.org',
+    url: 'blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f' },
+  { expected: 'ftp://example.org', url: 'ftp://example.org/foo' },
+  { expected: 'gopher://gopher.quux.org', url: 'gopher://gopher.quux.org/1/' },
+  { expected: 'http://example.org', url: 'http://example.org/foo' },
+  { expected: 'https://example.org', url: 'https://example.org/foo' },
+  { expected: 'ws://example.org', url: 'ws://example.org/foo' },
+  { expected: 'wss://example.org', url: 'wss://example.org/foo' },
+  { expected: 'null', url: 'file:///tmp/mock/path' },
+  { expected: 'null', url: 'npm://nodejs/rules' }
+].forEach((test) => {
+  assert.strictEqual(new URL(test.url).origin, test.expected);
+});
diff --git a/test/parallel/test-whatwg-url-searchparams-append.js b/test/parallel/test-whatwg-url-searchparams-append.js
new file mode 100644
index 00000000000000..6571f570588339
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-append.js
@@ -0,0 +1,73 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const URLSearchParams = require('url').URLSearchParams;
+const { test, assert_equals, assert_true } = require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-append.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+test(function() {
+    var params = new URLSearchParams();
+    params.append('a', 'b');
+    assert_equals(params + '', 'a=b');
+    params.append('a', 'b');
+    assert_equals(params + '', 'a=b&a=b');
+    params.append('a', 'c');
+    assert_equals(params + '', 'a=b&a=b&a=c');
+}, 'Append same name');
+test(function() {
+    var params = new URLSearchParams();
+    params.append('', '');
+    assert_equals(params + '', '=');
+    params.append('', '');
+    assert_equals(params + '', '=&=');
+}, 'Append empty strings');
+test(function() {
+    var params = new URLSearchParams();
+    params.append(null, null);
+    assert_equals(params + '', 'null=null');
+    params.append(null, null);
+    assert_equals(params + '', 'null=null&null=null');
+}, 'Append null');
+test(function() {
+    var params = new URLSearchParams();
+    params.append('first', 1);
+    params.append('second', 2);
+    params.append('third', '');
+    params.append('first', 10);
+    assert_true(params.has('first'), 'Search params object has name "first"');
+    assert_equals(params.get('first'), '1', 'Search params object has name "first" with value "1"');
+    assert_equals(params.get('second'), '2', 'Search params object has name "second" with value "2"');
+    assert_equals(params.get('third'), '', 'Search params object has name "third" with value ""');
+    params.append('first', 10);
+    assert_equals(params.get('first'), '1', 'Search params object has name "first" with value "1"');
+}, 'Append multiple');
+/* eslint-enable */
+
+// Tests below are not from WPT.
+{
+  const params = new URLSearchParams();
+  assert.throws(() => {
+    params.append.call(undefined);
+  }, /^TypeError: Value of "this" must be of type URLSearchParams$/);
+  assert.throws(() => {
+    params.append('a');
+  }, /^TypeError: The "name" and "value" arguments must be specified$/);
+
+  const obj = {
+    toString() { throw new Error('toString'); },
+    valueOf() { throw new Error('valueOf'); }
+  };
+  const sym = Symbol();
+  assert.throws(() => params.set(obj, 'b'), /^Error: toString$/);
+  assert.throws(() => params.set('a', obj), /^Error: toString$/);
+  assert.throws(() => params.set(sym, 'b'),
+                /^TypeError: Cannot convert a Symbol value to a string$/);
+  assert.throws(() => params.set('a', sym),
+                /^TypeError: Cannot convert a Symbol value to a string$/);
+}
diff --git a/test/parallel/test-whatwg-url-searchparams-constructor.js b/test/parallel/test-whatwg-url-searchparams-constructor.js
new file mode 100644
index 00000000000000..a3e15875276087
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-constructor.js
@@ -0,0 +1,248 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const URLSearchParams = require('url').URLSearchParams;
+const {
+  test, assert_equals, assert_true,
+  assert_false, assert_throws, assert_array_equals
+} = require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/54c3502d7b/url/urlsearchparams-constructor.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+var params;  // Strict mode fix for WPT.
+test(function() {
+    var params = new URLSearchParams();
+    assert_equals(params + '', '');
+    params = new URLSearchParams('');
+    assert_equals(params + '', '');
+    params = new URLSearchParams('a=b');
+    assert_equals(params + '', 'a=b');
+    params = new URLSearchParams(params);
+    assert_equals(params + '', 'a=b');
+}, 'Basic URLSearchParams construction');
+
+test(function() {
+    var params = new URLSearchParams()
+    assert_equals(params.toString(), "")
+}, "URLSearchParams constructor, no arguments")
+
+// test(() => {
+//     params = new URLSearchParams(DOMException.prototype);
+//     assert_equals(params.toString(), "INDEX_SIZE_ERR=1&DOMSTRING_SIZE_ERR=2&HIERARCHY_REQUEST_ERR=3&WRONG_DOCUMENT_ERR=4&INVALID_CHARACTER_ERR=5&NO_DATA_ALLOWED_ERR=6&NO_MODIFICATION_ALLOWED_ERR=7&NOT_FOUND_ERR=8&NOT_SUPPORTED_ERR=9&INUSE_ATTRIBUTE_ERR=10&INVALID_STATE_ERR=11&SYNTAX_ERR=12&INVALID_MODIFICATION_ERR=13&NAMESPACE_ERR=14&INVALID_ACCESS_ERR=15&VALIDATION_ERR=16&TYPE_MISMATCH_ERR=17&SECURITY_ERR=18&NETWORK_ERR=19&ABORT_ERR=20&URL_MISMATCH_ERR=21&QUOTA_EXCEEDED_ERR=22&TIMEOUT_ERR=23&INVALID_NODE_TYPE_ERR=24&DATA_CLONE_ERR=25")
+// }, "URLSearchParams constructor, DOMException.prototype as argument")
+
+test(() => {
+    params = new URLSearchParams('');
+    assert_true(params != null, 'constructor returned non-null value.');
+    assert_equals(params.__proto__, URLSearchParams.prototype, 'expected URLSearchParams.prototype as prototype.');
+}, "URLSearchParams constructor, empty string as argument")
+
+test(() => {
+    params = new URLSearchParams({});
+    assert_equals(params + '', "");
+}, 'URLSearchParams constructor, {} as argument');
+
+test(function() {
+    var params = new URLSearchParams('a=b');
+    assert_true(params != null, 'constructor returned non-null value.');
+    assert_true(params.has('a'), 'Search params object has name "a"');
+    assert_false(params.has('b'), 'Search params object has not got name "b"');
+    var params = new URLSearchParams('a=b&c');
+    assert_true(params != null, 'constructor returned non-null value.');
+    assert_true(params.has('a'), 'Search params object has name "a"');
+    assert_true(params.has('c'), 'Search params object has name "c"');
+    var params = new URLSearchParams('&a&&& &&&&&a+b=& c&m%c3%b8%c3%b8');
+    assert_true(params != null, 'constructor returned non-null value.');
+    assert_true(params.has('a'), 'Search params object has name "a"');
+    assert_true(params.has('a b'), 'Search params object has name "a b"');
+    assert_true(params.has(' '), 'Search params object has name " "');
+    assert_false(params.has('c'), 'Search params object did not have the name "c"');
+    assert_true(params.has(' c'), 'Search params object has name " c"');
+    assert_true(params.has('møø'), 'Search params object has name "møø"');
+}, 'URLSearchParams constructor, string.');
+
+test(function() {
+    var seed = new URLSearchParams('a=b&c=d');
+    var params = new URLSearchParams(seed);
+    assert_true(params != null, 'constructor returned non-null value.');
+    assert_equals(params.get('a'), 'b');
+    assert_equals(params.get('c'), 'd');
+    assert_false(params.has('d'));
+    // The name-value pairs are copied when created; later updates
+    // should not be observable.
+    seed.append('e', 'f');
+    assert_false(params.has('e'));
+    params.append('g', 'h');
+    assert_false(seed.has('g'));
+}, 'URLSearchParams constructor, object.');
+
+test(function() {
+    var params = new URLSearchParams('a=b+c');
+    assert_equals(params.get('a'), 'b c');
+    params = new URLSearchParams('a+b=c');
+    assert_equals(params.get('a b'), 'c');
+}, 'Parse +');
+
+test(function() {
+    const testValue = '+15555555555';
+    const params = new URLSearchParams();
+    params.set('query', testValue);
+    var newParams = new URLSearchParams(params.toString());
+
+    assert_equals(params.toString(), 'query=%2B15555555555');
+    assert_equals(params.get('query'), testValue);
+    assert_equals(newParams.get('query'), testValue);
+}, 'Parse encoded +');
+
+test(function() {
+    var params = new URLSearchParams('a=b c');
+    assert_equals(params.get('a'), 'b c');
+    params = new URLSearchParams('a b=c');
+    assert_equals(params.get('a b'), 'c');
+}, 'Parse space');
+
+test(function() {
+    var params = new URLSearchParams('a=b%20c');
+    assert_equals(params.get('a'), 'b c');
+    params = new URLSearchParams('a%20b=c');
+    assert_equals(params.get('a b'), 'c');
+}, 'Parse %20');
+
+test(function() {
+    var params = new URLSearchParams('a=b\0c');
+    assert_equals(params.get('a'), 'b\0c');
+    params = new URLSearchParams('a\0b=c');
+    assert_equals(params.get('a\0b'), 'c');
+}, 'Parse \\0');
+
+test(function() {
+    var params = new URLSearchParams('a=b%00c');
+    assert_equals(params.get('a'), 'b\0c');
+    params = new URLSearchParams('a%00b=c');
+    assert_equals(params.get('a\0b'), 'c');
+}, 'Parse %00');
+
+test(function() {
+    var params = new URLSearchParams('a=b\u2384');
+    assert_equals(params.get('a'), 'b\u2384');
+    params = new URLSearchParams('a\u2384b=c');
+    assert_equals(params.get('a\u2384b'), 'c');
+}, 'Parse \u2384');  // Unicode Character 'COMPOSITION SYMBOL' (U+2384)
+
+test(function() {
+    var params = new URLSearchParams('a=b%e2%8e%84');
+    assert_equals(params.get('a'), 'b\u2384');
+    params = new URLSearchParams('a%e2%8e%84b=c');
+    assert_equals(params.get('a\u2384b'), 'c');
+}, 'Parse %e2%8e%84');  // Unicode Character 'COMPOSITION SYMBOL' (U+2384)
+
+test(function() {
+    var params = new URLSearchParams('a=b\uD83D\uDCA9c');
+    assert_equals(params.get('a'), 'b\uD83D\uDCA9c');
+    params = new URLSearchParams('a\uD83D\uDCA9b=c');
+    assert_equals(params.get('a\uD83D\uDCA9b'), 'c');
+}, 'Parse \uD83D\uDCA9');  // Unicode Character 'PILE OF POO' (U+1F4A9)
+
+test(function() {
+    var params = new URLSearchParams('a=b%f0%9f%92%a9c');
+    assert_equals(params.get('a'), 'b\uD83D\uDCA9c');
+    params = new URLSearchParams('a%f0%9f%92%a9b=c');
+    assert_equals(params.get('a\uD83D\uDCA9b'), 'c');
+}, 'Parse %f0%9f%92%a9');  // Unicode Character 'PILE OF POO' (U+1F4A9)
+
+test(function() {
+    var params = new URLSearchParams([]);
+    assert_true(params != null, 'constructor returned non-null value.');
+    params = new URLSearchParams([['a', 'b'], ['c', 'd']]);
+    assert_equals(params.get("a"), "b");
+    assert_equals(params.get("c"), "d");
+    assert_throws(new TypeError(), function() { new URLSearchParams([[1]]); });
+    assert_throws(new TypeError(), function() { new URLSearchParams([[1,2,3]]); });
+}, "Constructor with sequence of sequences of strings");
+
+[
+  { "input": {"+": "%C2"}, "output": [["+", "%C2"]], "name": "object with +" },
+  { "input": {c: "x", a: "?"}, "output": [["c", "x"], ["a", "?"]], "name": "object with two keys" },
+  { "input": [["c", "x"], ["a", "?"]], "output": [["c", "x"], ["a", "?"]], "name": "array with two keys" },
+  { "input": {"a\0b": "42", "c\uD83D": "23", "d\u1234": "foo"}, "output": [["a\0b", "42"], ["c\uFFFD", "23"], ["d\u1234", "foo"]], "name": "object with NULL, non-ASCII, and surrogate keys" }
+].forEach((val) => {
+    test(() => {
+        let params = new URLSearchParams(val.input),
+            i = 0
+        for (let param of params) {
+            assert_array_equals(param, val.output[i])
+            i++
+        }
+    }, "Construct with " + val.name)
+})
+
+test(() => {
+  params = new URLSearchParams()
+  params[Symbol.iterator] = function *() {
+    yield ["a", "b"]
+  }
+  let params2 = new URLSearchParams(params)
+  assert_equals(params2.get("a"), "b")
+}, "Custom [Symbol.iterator]")
+/* eslint-enable */
+
+// Tests below are not from WPT.
+function makeIterableFunc(array) {
+  return Object.assign(() => {}, {
+    [Symbol.iterator]() {
+      return array[Symbol.iterator]();
+    }
+  });
+}
+
+{
+  const iterableError = /^TypeError: Query pairs must be iterable$/;
+  const tupleError =
+    /^TypeError: Each query pair must be an iterable \[name, value] tuple$/;
+
+  let params;
+  params = new URLSearchParams(undefined);
+  assert.strictEqual(params.toString(), '');
+  params = new URLSearchParams(null);
+  assert.strictEqual(params.toString(), '');
+  params = new URLSearchParams(
+    makeIterableFunc([['key', 'val'], ['key2', 'val2']])
+  );
+  assert.strictEqual(params.toString(), 'key=val&key2=val2');
+  params = new URLSearchParams(
+    makeIterableFunc([['key', 'val'], ['key2', 'val2']].map(makeIterableFunc))
+  );
+  assert.strictEqual(params.toString(), 'key=val&key2=val2');
+  assert.throws(() => new URLSearchParams([[1]]), tupleError);
+  assert.throws(() => new URLSearchParams([[1, 2, 3]]), tupleError);
+  assert.throws(() => new URLSearchParams({ [Symbol.iterator]: 42 }),
+                iterableError);
+  assert.throws(() => new URLSearchParams([{}]), tupleError);
+  assert.throws(() => new URLSearchParams(['a']), tupleError);
+  assert.throws(() => new URLSearchParams([null]), tupleError);
+  assert.throws(() => new URLSearchParams([{ [Symbol.iterator]: 42 }]),
+                tupleError);
+}
+
+{
+  const obj = {
+    toString() { throw new Error('toString'); },
+    valueOf() { throw new Error('valueOf'); }
+  };
+  const sym = Symbol();
+  const toStringError = /^Error: toString$/;
+  const symbolError = /^TypeError: Cannot convert a Symbol value to a string$/;
+
+  assert.throws(() => new URLSearchParams({ a: obj }), toStringError);
+  assert.throws(() => new URLSearchParams([['a', obj]]), toStringError);
+  assert.throws(() => new URLSearchParams(sym), symbolError);
+  assert.throws(() => new URLSearchParams({ [sym]: 'a' }), symbolError);
+  assert.throws(() => new URLSearchParams({ a: sym }), symbolError);
+  assert.throws(() => new URLSearchParams([[sym, 'a']]), symbolError);
+  assert.throws(() => new URLSearchParams([['a', sym]]), symbolError);
+}
diff --git a/test/parallel/test-whatwg-url-searchparams-delete.js b/test/parallel/test-whatwg-url-searchparams-delete.js
new file mode 100644
index 00000000000000..bd52a13e9174b3
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-delete.js
@@ -0,0 +1,92 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const { URL, URLSearchParams } = require('url');
+const { test, assert_equals, assert_true, assert_false } =
+  require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/70a0898763/url/urlsearchparams-delete.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+test(function() {
+    var params = new URLSearchParams('a=b&c=d');
+    params.delete('a');
+    assert_equals(params + '', 'c=d');
+    params = new URLSearchParams('a=a&b=b&a=a&c=c');
+    params.delete('a');
+    assert_equals(params + '', 'b=b&c=c');
+    params = new URLSearchParams('a=a&=&b=b&c=c');
+    params.delete('');
+    assert_equals(params + '', 'a=a&b=b&c=c');
+    params = new URLSearchParams('a=a&null=null&b=b');
+    params.delete(null);
+    assert_equals(params + '', 'a=a&b=b');
+    params = new URLSearchParams('a=a&undefined=undefined&b=b');
+    params.delete(undefined);
+    assert_equals(params + '', 'a=a&b=b');
+}, 'Delete basics');
+
+test(function() {
+    var params = new URLSearchParams();
+    params.append('first', 1);
+    assert_true(params.has('first'), 'Search params object has name "first"');
+    assert_equals(params.get('first'), '1', 'Search params object has name "first" with value "1"');
+    params.delete('first');
+    assert_false(params.has('first'), 'Search params object has no "first" name');
+    params.append('first', 1);
+    params.append('first', 10);
+    params.delete('first');
+    assert_false(params.has('first'), 'Search params object has no "first" name');
+}, 'Deleting appended multiple');
+
+test(function() {
+    var url = new URL('http://example.com/?param1&param2');
+    url.searchParams.delete('param1');
+    url.searchParams.delete('param2');
+    assert_equals(url.href, 'http://example.com/', 'url.href does not have ?');
+    assert_equals(url.search, '', 'url.search does not have ?');
+}, 'Deleting all params removes ? from URL');
+
+test(function() {
+    var url = new URL('http://example.com/?');
+    url.searchParams.delete('param1');
+    assert_equals(url.href, 'http://example.com/', 'url.href does not have ?');
+    assert_equals(url.search, '', 'url.search does not have ?');
+}, 'Removing non-existent param removes ? from URL');
+/* eslint-enable */
+
+// Tests below are not from WPT.
+{
+  const params = new URLSearchParams();
+  assert.throws(() => {
+    params.delete.call(undefined);
+  }, /^TypeError: Value of "this" must be of type URLSearchParams$/);
+  assert.throws(() => {
+    params.delete();
+  }, /^TypeError: The "name" argument must be specified$/);
+
+  const obj = {
+    toString() { throw new Error('toString'); },
+    valueOf() { throw new Error('valueOf'); }
+  };
+  const sym = Symbol();
+  assert.throws(() => params.delete(obj), /^Error: toString$/);
+  assert.throws(() => params.delete(sym),
+                /^TypeError: Cannot convert a Symbol value to a string$/);
+}
+
+// https://github.com/nodejs/node/issues/10480
+// Emptying searchParams should correctly update url's query
+{
+  const url = new URL('http://domain?var=1&var=2&var=3');
+  for (const param of url.searchParams.keys()) {
+    url.searchParams.delete(param);
+  }
+  assert.strictEqual(url.searchParams.toString(), '');
+  assert.strictEqual(url.search, '');
+  assert.strictEqual(url.href, 'http://domain/');
+}
diff --git a/test/parallel/test-whatwg-url-searchparams-entries.js b/test/parallel/test-whatwg-url-searchparams-entries.js
new file mode 100644
index 00000000000000..4e73b92b517fd9
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-entries.js
@@ -0,0 +1,34 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const URLSearchParams = require('url').URLSearchParams;
+
+// Tests below are not from WPT.
+const params = new URLSearchParams('a=b&c=d');
+const entries = params.entries();
+assert.strictEqual(typeof entries[Symbol.iterator], 'function');
+assert.strictEqual(entries[Symbol.iterator](), entries);
+assert.deepStrictEqual(entries.next(), {
+  value: ['a', 'b'],
+  done: false
+});
+assert.deepStrictEqual(entries.next(), {
+  value: ['c', 'd'],
+  done: false
+});
+assert.deepStrictEqual(entries.next(), {
+  value: undefined,
+  done: true
+});
+assert.deepStrictEqual(entries.next(), {
+  value: undefined,
+  done: true
+});
+
+assert.throws(() => {
+  entries.next.call(undefined);
+}, /^TypeError: Value of "this" must be of type URLSearchParamsIterator$/);
+assert.throws(() => {
+  params.entries.call(undefined);
+}, /^TypeError: Value of "this" must be of type URLSearchParams$/);
diff --git a/test/parallel/test-whatwg-url-searchparams-foreach.js b/test/parallel/test-whatwg-url-searchparams-foreach.js
new file mode 100644
index 00000000000000..06f21723a6cd2f
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-foreach.js
@@ -0,0 +1,56 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const { URL, URLSearchParams } = require('url');
+const { test, assert_array_equals, assert_unreached } =
+  require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/a8b2b1e/url/urlsearchparams-foreach.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+var i;  // Strict mode fix for WPT.
+test(function() {
+    var params = new URLSearchParams('a=1&b=2&c=3');
+    var keys = [];
+    var values = [];
+    params.forEach(function(value, key) {
+        keys.push(key);
+        values.push(value);
+    });
+    assert_array_equals(keys, ['a', 'b', 'c']);
+    assert_array_equals(values, ['1', '2', '3']);
+}, "ForEach Check");
+
+test(function() {
+    let a = new URL("http://a.b/c?a=1&b=2&c=3&d=4");
+    let b = a.searchParams;
+    var c = [];
+    for (i of b) {
+        a.search = "x=1&y=2&z=3";
+        c.push(i);
+    }
+    assert_array_equals(c[0], ["a","1"]);
+    assert_array_equals(c[1], ["y","2"]);
+    assert_array_equals(c[2], ["z","3"]);
+}, "For-of Check");
+
+test(function() {
+    let a = new URL("http://a.b/c");
+    let b = a.searchParams;
+    for (i of b) {
+        assert_unreached(i);
+    }
+}, "empty");
+/* eslint-enable */
+
+// Tests below are not from WPT.
+{
+  const params = new URLSearchParams();
+  assert.throws(() => {
+    params.forEach.call(undefined);
+  }, /^TypeError: Value of "this" must be of type URLSearchParams$/);
+}
diff --git a/test/parallel/test-whatwg-url-searchparams-get.js b/test/parallel/test-whatwg-url-searchparams-get.js
new file mode 100644
index 00000000000000..b096a69a6071a0
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-get.js
@@ -0,0 +1,55 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const URLSearchParams = require('url').URLSearchParams;
+const { test, assert_equals, assert_true } = require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-get.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+test(function() {
+    var params = new URLSearchParams('a=b&c=d');
+    assert_equals(params.get('a'), 'b');
+    assert_equals(params.get('c'), 'd');
+    assert_equals(params.get('e'), null);
+    params = new URLSearchParams('a=b&c=d&a=e');
+    assert_equals(params.get('a'), 'b');
+    params = new URLSearchParams('=b&c=d');
+    assert_equals(params.get(''), 'b');
+    params = new URLSearchParams('a=&c=d&a=e');
+    assert_equals(params.get('a'), '');
+}, 'Get basics');
+
+test(function() {
+    var params = new URLSearchParams('first=second&third&&');
+    assert_true(params != null, 'constructor returned non-null value.');
+    assert_true(params.has('first'), 'Search params object has name "first"');
+    assert_equals(params.get('first'), 'second', 'Search params object has name "first" with value "second"');
+    assert_equals(params.get('third'), '', 'Search params object has name "third" with the empty value.');
+    assert_equals(params.get('fourth'), null, 'Search params object has no "fourth" name and value.');
+}, 'More get() basics');
+/* eslint-enable */
+
+// Tests below are not from WPT.
+{
+  const params = new URLSearchParams();
+  assert.throws(() => {
+    params.get.call(undefined);
+  }, /^TypeError: Value of "this" must be of type URLSearchParams$/);
+  assert.throws(() => {
+    params.get();
+  }, /^TypeError: The "name" argument must be specified$/);
+
+  const obj = {
+    toString() { throw new Error('toString'); },
+    valueOf() { throw new Error('valueOf'); }
+  };
+  const sym = Symbol();
+  assert.throws(() => params.get(obj), /^Error: toString$/);
+  assert.throws(() => params.get(sym),
+                /^TypeError: Cannot convert a Symbol value to a string$/);
+}
diff --git a/test/parallel/test-whatwg-url-searchparams-getall.js b/test/parallel/test-whatwg-url-searchparams-getall.js
new file mode 100644
index 00000000000000..acf5108459cf61
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-getall.js
@@ -0,0 +1,60 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const URLSearchParams = require('url').URLSearchParams;
+const { test, assert_equals, assert_true, assert_array_equals } =
+  require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-getall.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+test(function() {
+    var params = new URLSearchParams('a=b&c=d');
+    assert_array_equals(params.getAll('a'), ['b']);
+    assert_array_equals(params.getAll('c'), ['d']);
+    assert_array_equals(params.getAll('e'), []);
+    params = new URLSearchParams('a=b&c=d&a=e');
+    assert_array_equals(params.getAll('a'), ['b', 'e']);
+    params = new URLSearchParams('=b&c=d');
+    assert_array_equals(params.getAll(''), ['b']);
+    params = new URLSearchParams('a=&c=d&a=e');
+    assert_array_equals(params.getAll('a'), ['', 'e']);
+}, 'getAll() basics');
+
+test(function() {
+    var params = new URLSearchParams('a=1&a=2&a=3&a');
+    assert_true(params.has('a'), 'Search params object has name "a"');
+    var matches = params.getAll('a');
+    assert_true(matches && matches.length == 4, 'Search params object has values for name "a"');
+    assert_array_equals(matches, ['1', '2', '3', ''], 'Search params object has expected name "a" values');
+    params.set('a', 'one');
+    assert_equals(params.get('a'), 'one', 'Search params object has name "a" with value "one"');
+    var matches = params.getAll('a');
+    assert_true(matches && matches.length == 1, 'Search params object has values for name "a"');
+    assert_array_equals(matches, ['one'], 'Search params object has expected name "a" values');
+}, 'getAll() multiples');
+/* eslint-enable */
+
+// Tests below are not from WPT.
+{
+  const params = new URLSearchParams();
+  assert.throws(() => {
+    params.getAll.call(undefined);
+  }, /^TypeError: Value of "this" must be of type URLSearchParams$/);
+  assert.throws(() => {
+    params.getAll();
+  }, /^TypeError: The "name" argument must be specified$/);
+
+  const obj = {
+    toString() { throw new Error('toString'); },
+    valueOf() { throw new Error('valueOf'); }
+  };
+  const sym = Symbol();
+  assert.throws(() => params.getAll(obj), /^Error: toString$/);
+  assert.throws(() => params.getAll(sym),
+                /^TypeError: Cannot convert a Symbol value to a string$/);
+}
diff --git a/test/parallel/test-whatwg-url-searchparams-has.js b/test/parallel/test-whatwg-url-searchparams-has.js
new file mode 100644
index 00000000000000..0fdd88af64c5af
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-has.js
@@ -0,0 +1,58 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const URLSearchParams = require('url').URLSearchParams;
+const { test, assert_false, assert_true } = require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-has.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+test(function() {
+    var params = new URLSearchParams('a=b&c=d');
+    assert_true(params.has('a'));
+    assert_true(params.has('c'));
+    assert_false(params.has('e'));
+    params = new URLSearchParams('a=b&c=d&a=e');
+    assert_true(params.has('a'));
+    params = new URLSearchParams('=b&c=d');
+    assert_true(params.has(''));
+    params = new URLSearchParams('null=a');
+    assert_true(params.has(null));
+}, 'Has basics');
+
+test(function() {
+    var params = new URLSearchParams('a=b&c=d&&');
+    params.append('first', 1);
+    params.append('first', 2);
+    assert_true(params.has('a'), 'Search params object has name "a"');
+    assert_true(params.has('c'), 'Search params object has name "c"');
+    assert_true(params.has('first'), 'Search params object has name "first"');
+    assert_false(params.has('d'), 'Search params object has no name "d"');
+    params.delete('first');
+    assert_false(params.has('first'), 'Search params object has no name "first"');
+}, 'has() following delete()');
+/* eslint-enable */
+
+// Tests below are not from WPT.
+{
+  const params = new URLSearchParams();
+  assert.throws(() => {
+    params.has.call(undefined);
+  }, /^TypeError: Value of "this" must be of type URLSearchParams$/);
+  assert.throws(() => {
+    params.has();
+  }, /^TypeError: The "name" argument must be specified$/);
+
+  const obj = {
+    toString() { throw new Error('toString'); },
+    valueOf() { throw new Error('valueOf'); }
+  };
+  const sym = Symbol();
+  assert.throws(() => params.has(obj), /^Error: toString$/);
+  assert.throws(() => params.has(sym),
+                /^TypeError: Cannot convert a Symbol value to a string$/);
+}
diff --git a/test/parallel/test-whatwg-url-searchparams-inspect.js b/test/parallel/test-whatwg-url-searchparams-inspect.js
new file mode 100644
index 00000000000000..163fa185ede58d
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-inspect.js
@@ -0,0 +1,29 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const util = require('util');
+const URLSearchParams = require('url').URLSearchParams;
+
+// Tests below are not from WPT.
+const sp = new URLSearchParams('?a=a&b=b&b=c');
+assert.strictEqual(util.inspect(sp),
+                   "URLSearchParams { 'a' => 'a', 'b' => 'b', 'b' => 'c' }");
+assert.strictEqual(util.inspect(sp.keys()),
+                   "URLSearchParamsIterator { 'a', 'b', 'b' }");
+assert.strictEqual(util.inspect(sp.values()),
+                   "URLSearchParamsIterator { 'a', 'b', 'c' }");
+assert.strictEqual(util.inspect(sp.keys(), { breakLength: 1 }),
+                   "URLSearchParamsIterator {\n  'a',\n  'b',\n  'b' }");
+
+const iterator = sp.entries();
+assert.strictEqual(util.inspect(iterator),
+                   "URLSearchParamsIterator { [ 'a', 'a' ], [ 'b', 'b' ], " +
+                                             "[ 'b', 'c' ] }");
+iterator.next();
+assert.strictEqual(util.inspect(iterator),
+                   "URLSearchParamsIterator { [ 'b', 'b' ], [ 'b', 'c' ] }");
+iterator.next();
+iterator.next();
+assert.strictEqual(util.inspect(iterator),
+                   'URLSearchParamsIterator {  }');
diff --git a/test/parallel/test-whatwg-url-searchparams-keys.js b/test/parallel/test-whatwg-url-searchparams-keys.js
new file mode 100644
index 00000000000000..af044a260874ac
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-keys.js
@@ -0,0 +1,35 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const URLSearchParams = require('url').URLSearchParams;
+
+// Tests below are not from WPT.
+const params = new URLSearchParams('a=b&c=d');
+const keys = params.keys();
+
+assert.strictEqual(typeof keys[Symbol.iterator], 'function');
+assert.strictEqual(keys[Symbol.iterator](), keys);
+assert.deepStrictEqual(keys.next(), {
+  value: 'a',
+  done: false
+});
+assert.deepStrictEqual(keys.next(), {
+  value: 'c',
+  done: false
+});
+assert.deepStrictEqual(keys.next(), {
+  value: undefined,
+  done: true
+});
+assert.deepStrictEqual(keys.next(), {
+  value: undefined,
+  done: true
+});
+
+assert.throws(() => {
+  keys.next.call(undefined);
+}, /^TypeError: Value of "this" must be of type URLSearchParamsIterator$/);
+assert.throws(() => {
+  params.keys.call(undefined);
+}, /^TypeError: Value of "this" must be of type URLSearchParams$/);
diff --git a/test/parallel/test-whatwg-url-searchparams-set.js b/test/parallel/test-whatwg-url-searchparams-set.js
new file mode 100644
index 00000000000000..8a6c31bbe66487
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-set.js
@@ -0,0 +1,59 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const URLSearchParams = require('url').URLSearchParams;
+const { test, assert_equals, assert_true } = require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-set.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+test(function() {
+    var params = new URLSearchParams('a=b&c=d');
+    params.set('a', 'B');
+    assert_equals(params + '', 'a=B&c=d');
+    params = new URLSearchParams('a=b&c=d&a=e');
+    params.set('a', 'B');
+    assert_equals(params + '', 'a=B&c=d')
+    params.set('e', 'f');
+    assert_equals(params + '', 'a=B&c=d&e=f')
+}, 'Set basics');
+
+test(function() {
+    var params = new URLSearchParams('a=1&a=2&a=3');
+    assert_true(params.has('a'), 'Search params object has name "a"');
+    assert_equals(params.get('a'), '1', 'Search params object has name "a" with value "1"');
+    params.set('first', 4);
+    assert_true(params.has('a'), 'Search params object has name "a"');
+    assert_equals(params.get('a'), '1', 'Search params object has name "a" with value "1"');
+    params.set('a', 4);
+    assert_true(params.has('a'), 'Search params object has name "a"');
+    assert_equals(params.get('a'), '4', 'Search params object has name "a" with value "4"');
+}, 'URLSearchParams.set');
+/* eslint-enable */
+
+// Tests below are not from WPT.
+{
+  const params = new URLSearchParams();
+  assert.throws(() => {
+    params.set.call(undefined);
+  }, /^TypeError: Value of "this" must be of type URLSearchParams$/);
+  assert.throws(() => {
+    params.set('a');
+  }, /^TypeError: The "name" and "value" arguments must be specified$/);
+
+  const obj = {
+    toString() { throw new Error('toString'); },
+    valueOf() { throw new Error('valueOf'); }
+  };
+  const sym = Symbol();
+  assert.throws(() => params.append(obj, 'b'), /^Error: toString$/);
+  assert.throws(() => params.append('a', obj), /^Error: toString$/);
+  assert.throws(() => params.append(sym, 'b'),
+                /^TypeError: Cannot convert a Symbol value to a string$/);
+  assert.throws(() => params.append('a', sym),
+                /^TypeError: Cannot convert a Symbol value to a string$/);
+}
diff --git a/test/parallel/test-whatwg-url-searchparams-sort.js b/test/parallel/test-whatwg-url-searchparams-sort.js
new file mode 100644
index 00000000000000..1122f08dcc0434
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-sort.js
@@ -0,0 +1,105 @@
+'use strict';
+
+require('../common');
+const { URL, URLSearchParams } = require('url');
+const { test, assert_equals, assert_array_equals } = require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/70a0898763/url/urlsearchparams-sort.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+[
+  {
+    "input": "z=b&a=b&z=a&a=a",
+    "output": [["a", "b"], ["a", "a"], ["z", "b"], ["z", "a"]]
+  },
+  {
+    "input": "\uFFFD=x&\uFFFC&\uFFFD=a",
+    "output": [["\uFFFC", ""], ["\uFFFD", "x"], ["\uFFFD", "a"]]
+  },
+  {
+    "input": "ffi&🌈", // 🌈 > code point, but < code unit because two code units
+    "output": [["🌈", ""], ["ffi", ""]]
+  },
+  {
+    "input": "é&e\uFFFD&e\u0301",
+    "output": [["e\u0301", ""], ["e\uFFFD", ""], ["é", ""]]
+  },
+  {
+    "input": "z=z&a=a&z=y&a=b&z=x&a=c&z=w&a=d&z=v&a=e&z=u&a=f&z=t&a=g",
+    "output": [["a", "a"], ["a", "b"], ["a", "c"], ["a", "d"], ["a", "e"], ["a", "f"], ["a", "g"], ["z", "z"], ["z", "y"], ["z", "x"], ["z", "w"], ["z", "v"], ["z", "u"], ["z", "t"]]
+  }
+].forEach((val) => {
+  test(() => {
+    let params = new URLSearchParams(val.input),
+        i = 0
+    params.sort()
+    for(let param of params) {
+      assert_array_equals(param, val.output[i])
+      i++
+    }
+  }, `Parse and sort: ${val.input}`)
+
+  test(() => {
+    let url = new URL(`?${val.input}`, "https://example/")
+    url.searchParams.sort()
+    let params = new URLSearchParams(url.search),
+        i = 0
+    for(let param of params) {
+      assert_array_equals(param, val.output[i])
+      i++
+    }
+  }, `URL parse and sort: ${val.input}`)
+})
+
+test(function() {
+  const url = new URL("http://example.com/?")
+  url.searchParams.sort()
+  assert_equals(url.href, "http://example.com/")
+  assert_equals(url.search, "")
+}, "Sorting non-existent params removes ? from URL")
+/* eslint-enable */
+
+// Tests below are not from WPT.
+
+// Test bottom-up iterative stable merge sort
+const tests = [{ input: '', output: [] }];
+const pairs = [];
+for (let i = 10; i < 100; i++) {
+  pairs.push([`a${i}`, 'b']);
+  tests[0].output.push([`a${i}`, 'b']);
+}
+tests[0].input = pairs.sort(() => Math.random() > 0.5)
+  .map((pair) => pair.join('=')).join('&');
+
+tests.push(
+  {
+    'input': 'z=a&=b&c=d',
+    'output': [['', 'b'], ['c', 'd'], ['z', 'a']]
+  }
+);
+
+tests.forEach((val) => {
+  test(() => {
+    const params = new URLSearchParams(val.input);
+    let i = 0;
+    params.sort();
+    for (const param of params) {
+      assert_array_equals(param, val.output[i]);
+      i++;
+    }
+  }, `Parse and sort: ${val.input}`);
+
+  test(() => {
+    const url = new URL(`?${val.input}`, 'https://example/');
+    url.searchParams.sort();
+    const params = new URLSearchParams(url.search);
+    let i = 0;
+    for (const param of params) {
+      assert_array_equals(param, val.output[i]);
+      i++;
+    }
+  }, `URL parse and sort: ${val.input}`);
+});
diff --git a/test/parallel/test-whatwg-url-searchparams-stringifier.js b/test/parallel/test-whatwg-url-searchparams-stringifier.js
new file mode 100644
index 00000000000000..c355a2c9a9c29c
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-stringifier.js
@@ -0,0 +1,132 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const URLSearchParams = require('url').URLSearchParams;
+const { test, assert_equals } = require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-stringifier.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+test(function() {
+    var params = new URLSearchParams();
+    params.append('a', 'b c');
+    assert_equals(params + '', 'a=b+c');
+    params.delete('a');
+    params.append('a b', 'c');
+    assert_equals(params + '', 'a+b=c');
+}, 'Serialize space');
+
+test(function() {
+    var params = new URLSearchParams();
+    params.append('a', '');
+    assert_equals(params + '', 'a=');
+    params.append('a', '');
+    assert_equals(params + '', 'a=&a=');
+    params.append('', 'b');
+    assert_equals(params + '', 'a=&a=&=b');
+    params.append('', '');
+    assert_equals(params + '', 'a=&a=&=b&=');
+    params.append('', '');
+    assert_equals(params + '', 'a=&a=&=b&=&=');
+}, 'Serialize empty value');
+
+test(function() {
+    var params = new URLSearchParams();
+    params.append('', 'b');
+    assert_equals(params + '', '=b');
+    params.append('', 'b');
+    assert_equals(params + '', '=b&=b');
+}, 'Serialize empty name');
+
+test(function() {
+    var params = new URLSearchParams();
+    params.append('', '');
+    assert_equals(params + '', '=');
+    params.append('', '');
+    assert_equals(params + '', '=&=');
+}, 'Serialize empty name and value');
+
+test(function() {
+    var params = new URLSearchParams();
+    params.append('a', 'b+c');
+    assert_equals(params + '', 'a=b%2Bc');
+    params.delete('a');
+    params.append('a+b', 'c');
+    assert_equals(params + '', 'a%2Bb=c');
+}, 'Serialize +');
+
+test(function() {
+    var params = new URLSearchParams();
+    params.append('=', 'a');
+    assert_equals(params + '', '%3D=a');
+    params.append('b', '=');
+    assert_equals(params + '', '%3D=a&b=%3D');
+}, 'Serialize =');
+
+test(function() {
+    var params = new URLSearchParams();
+    params.append('&', 'a');
+    assert_equals(params + '', '%26=a');
+    params.append('b', '&');
+    assert_equals(params + '', '%26=a&b=%26');
+}, 'Serialize &');
+
+test(function() {
+    var params = new URLSearchParams();
+    params.append('a', '*-._');
+    assert_equals(params + '', 'a=*-._');
+    params.delete('a');
+    params.append('*-._', 'c');
+    assert_equals(params + '', '*-._=c');
+}, 'Serialize *-._');
+
+test(function() {
+    var params = new URLSearchParams();
+    params.append('a', 'b%c');
+    assert_equals(params + '', 'a=b%25c');
+    params.delete('a');
+    params.append('a%b', 'c');
+    assert_equals(params + '', 'a%25b=c');
+}, 'Serialize %');
+
+test(function() {
+    var params = new URLSearchParams();
+    params.append('a', 'b\0c');
+    assert_equals(params + '', 'a=b%00c');
+    params.delete('a');
+    params.append('a\0b', 'c');
+    assert_equals(params + '', 'a%00b=c');
+}, 'Serialize \\0');
+
+test(function() {
+    var params = new URLSearchParams();
+    params.append('a', 'b\uD83D\uDCA9c');
+    assert_equals(params + '', 'a=b%F0%9F%92%A9c');
+    params.delete('a');
+    params.append('a\uD83D\uDCA9b', 'c');
+    assert_equals(params + '', 'a%F0%9F%92%A9b=c');
+}, 'Serialize \uD83D\uDCA9');  // Unicode Character 'PILE OF POO' (U+1F4A9)
+
+test(function() {
+    var params;
+    params = new URLSearchParams('a=b&c=d&&e&&');
+    assert_equals(params.toString(), 'a=b&c=d&e=');
+    params = new URLSearchParams('a = b &a=b&c=d%20');
+    assert_equals(params.toString(), 'a+=+b+&a=b&c=d+');
+    // The lone '=' _does_ survive the roundtrip.
+    params = new URLSearchParams('a=&a=b');
+    assert_equals(params.toString(), 'a=&a=b');
+}, 'URLSearchParams.toString');
+/* eslint-enable */
+
+// Tests below are not from WPT.
+{
+  const params = new URLSearchParams();
+  assert.throws(() => {
+    params.toString.call(undefined);
+  }, /^TypeError: Value of "this" must be of type URLSearchParams$/);
+}
diff --git a/test/parallel/test-whatwg-url-searchparams-values.js b/test/parallel/test-whatwg-url-searchparams-values.js
new file mode 100644
index 00000000000000..2775231b8bda5d
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams-values.js
@@ -0,0 +1,35 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const URLSearchParams = require('url').URLSearchParams;
+
+// Tests below are not from WPT.
+const params = new URLSearchParams('a=b&c=d');
+const values = params.values();
+
+assert.strictEqual(typeof values[Symbol.iterator], 'function');
+assert.strictEqual(values[Symbol.iterator](), values);
+assert.deepStrictEqual(values.next(), {
+  value: 'b',
+  done: false
+});
+assert.deepStrictEqual(values.next(), {
+  value: 'd',
+  done: false
+});
+assert.deepStrictEqual(values.next(), {
+  value: undefined,
+  done: true
+});
+assert.deepStrictEqual(values.next(), {
+  value: undefined,
+  done: true
+});
+
+assert.throws(() => {
+  values.next.call(undefined);
+}, /^TypeError: Value of "this" must be of type URLSearchParamsIterator$/);
+assert.throws(() => {
+  params.values.call(undefined);
+}, /^TypeError: Value of "this" must be of type URLSearchParams$/);
diff --git a/test/parallel/test-whatwg-url-searchparams.js b/test/parallel/test-whatwg-url-searchparams.js
new file mode 100644
index 00000000000000..b6861273e71a0e
--- /dev/null
+++ b/test/parallel/test-whatwg-url-searchparams.js
@@ -0,0 +1,106 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const { URL, URLSearchParams } = require('url');
+const fixtures = require('../common/fixtures');
+
+// Tests below are not from WPT.
+const serialized = 'a=a&a=1&a=true&a=undefined&a=null&a=%EF%BF%BD' +
+                   '&a=%EF%BF%BD&a=%F0%9F%98%80&a=%EF%BF%BD%EF%BF%BD' +
+                   '&a=%5Bobject+Object%5D';
+const values = ['a', 1, true, undefined, null, '\uD83D', '\uDE00',
+                '\uD83D\uDE00', '\uDE00\uD83D', {}];
+const normalizedValues = ['a', '1', 'true', 'undefined', 'null', '\uFFFD',
+                          '\uFFFD', '\uD83D\uDE00', '\uFFFD\uFFFD',
+                          '[object Object]'];
+
+const m = new URL('http://example.org');
+const sp = m.searchParams;
+
+assert(sp);
+assert.strictEqual(sp.toString(), '');
+assert.strictEqual(m.search, '');
+
+assert(!sp.has('a'));
+values.forEach((i) => sp.set('a', i));
+assert(sp.has('a'));
+assert.strictEqual(sp.get('a'), '[object Object]');
+sp.delete('a');
+assert(!sp.has('a'));
+
+m.search = '';
+assert.strictEqual(sp.toString(), '');
+
+values.forEach((i) => sp.append('a', i));
+assert(sp.has('a'));
+assert.strictEqual(sp.getAll('a').length, values.length);
+assert.strictEqual(sp.get('a'), 'a');
+
+assert.strictEqual(sp.toString(), serialized);
+
+assert.strictEqual(m.search, `?${serialized}`);
+
+assert.strictEqual(sp[Symbol.iterator], sp.entries);
+
+let key, val;
+let n = 0;
+for ([key, val] of sp) {
+  assert.strictEqual(key, 'a', n);
+  assert.strictEqual(val, normalizedValues[n], n);
+  n++;
+}
+n = 0;
+for (key of sp.keys()) {
+  assert.strictEqual(key, 'a', n);
+  n++;
+}
+n = 0;
+for (val of sp.values()) {
+  assert.strictEqual(val, normalizedValues[n], n);
+  n++;
+}
+n = 0;
+sp.forEach(function(val, key, obj) {
+  assert.strictEqual(this, undefined, n);
+  assert.strictEqual(key, 'a', n);
+  assert.strictEqual(val, normalizedValues[n], n);
+  assert.strictEqual(obj, sp, n);
+  n++;
+});
+sp.forEach(function() {
+  assert.strictEqual(this, m);
+}, m);
+
+{
+  const callbackErr = /^TypeError: Callback must be a function$/;
+  assert.throws(() => sp.forEach(), callbackErr);
+  assert.throws(() => sp.forEach(1), callbackErr);
+}
+
+m.search = '?a=a&b=b';
+assert.strictEqual(sp.toString(), 'a=a&b=b');
+
+const tests = require(fixtures.path('url-searchparams.js'));
+
+for (const [input, expected, parsed] of tests) {
+  if (input[0] !== '?') {
+    const sp = new URLSearchParams(input);
+    assert.strictEqual(String(sp), expected);
+    assert.deepStrictEqual(Array.from(sp), parsed);
+
+    m.search = input;
+    assert.strictEqual(String(m.searchParams), expected);
+    assert.deepStrictEqual(Array.from(m.searchParams), parsed);
+  }
+
+  {
+    const sp = new URLSearchParams(`?${input}`);
+    assert.strictEqual(String(sp), expected);
+    assert.deepStrictEqual(Array.from(sp), parsed);
+
+    m.search = `?${input}`;
+    assert.strictEqual(String(m.searchParams), expected);
+    assert.deepStrictEqual(Array.from(m.searchParams), parsed);
+  }
+}
diff --git a/test/parallel/test-whatwg-url-setters.js b/test/parallel/test-whatwg-url-setters.js
new file mode 100644
index 00000000000000..9a25de59e7e55f
--- /dev/null
+++ b/test/parallel/test-whatwg-url-setters.js
@@ -0,0 +1,126 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasIntl) {
+  // A handful of the tests fail when ICU is not included.
+  common.skip('missing Intl');
+}
+
+const assert = require('assert');
+const URL = require('url').URL;
+const { test, assert_equals } = require('../common/wpt');
+const fixtures = require('../common/fixtures');
+
+const additionalTestCases =
+  require(fixtures.path('url-setter-tests-additional.js'));
+
+const request = {
+  response: require(fixtures.path('url-setter-tests'))
+};
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/8791bed/url/url-setters.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+function startURLSettersTests() {
+//   var setup = async_test("Loading data…")
+//   setup.step(function() {
+//     var request = new XMLHttpRequest()
+//     request.open("GET", "setters_tests.json")
+//     request.send()
+//     request.responseType = "json"
+//     request.onload = setup.step_func(function() {
+         runURLSettersTests(request.response)
+//       setup.done()
+//     })
+//   })
+}
+
+function runURLSettersTests(all_test_cases) {
+  for (var attribute_to_be_set in all_test_cases) {
+    if (attribute_to_be_set == "comment") {
+      continue;
+    }
+    var test_cases = all_test_cases[attribute_to_be_set];
+    for(var i = 0, l = test_cases.length; i < l; i++) {
+      var test_case = test_cases[i];
+      var name = `Setting <${test_case.href}>.${attribute_to_be_set}` +
+                 ` = '${test_case.new_value}'`;
+      if ("comment" in test_case) {
+        name += ` ${test_case.comment}`;
+      }
+      test(function() {
+        var url = new URL(test_case.href);
+        url[attribute_to_be_set] = test_case.new_value;
+        for (var attribute in test_case.expected) {
+          assert_equals(url[attribute], test_case.expected[attribute])
+        }
+      }, `URL: ${name}`);
+      // test(function() {
+      //   var url = document.createElement("a");
+      //   url.href = test_case.href;
+      //   url[attribute_to_be_set] = test_case.new_value;
+      //   for (var attribute in test_case.expected) {
+      //     assert_equals(url[attribute], test_case.expected[attribute])
+      //   }
+      // }, "<a>: " + name)
+      // test(function() {
+      //   var url = document.createElement("area");
+      //   url.href = test_case.href;
+      //   url[attribute_to_be_set] = test_case.new_value;
+      //   for (var attribute in test_case.expected) {
+      //     assert_equals(url[attribute], test_case.expected[attribute])
+      //   }
+      // }, "<area>: " + name)
+    }
+  }
+}
+
+startURLSettersTests()
+/* eslint-enable */
+
+// Tests below are not from WPT.
+
+{
+  for (const attributeToBeSet in additionalTestCases) {
+    if (attributeToBeSet === 'comment') {
+      continue;
+    }
+    const testCases = additionalTestCases[attributeToBeSet];
+    for (const testCase of testCases) {
+      let name = `Setting <${testCase.href}>.${attributeToBeSet}` +
+                 ` = "${testCase.new_value}"`;
+      if ('comment' in testCase) {
+        name += ` ${testCase.comment}`;
+      }
+      test(function() {
+        const url = new URL(testCase.href);
+        url[attributeToBeSet] = testCase.new_value;
+        for (const attribute in testCase.expected) {
+          assert_equals(url[attribute], testCase.expected[attribute]);
+        }
+      }, `URL: ${name}`);
+    }
+  }
+}
+
+{
+  const url = new URL('http://example.com/');
+  const obj = {
+    toString() { throw new Error('toString'); },
+    valueOf() { throw new Error('valueOf'); }
+  };
+  const sym = Symbol();
+  for (const name of Reflect.ownKeys(Object.getPrototypeOf(url))) {
+    if (Object.getOwnPropertyDescriptor(Object.getPrototypeOf(url), name).set) {
+      assert.throws(() => url[name] = obj,
+                    /^Error: toString$/,
+                    `url.${name} = { toString() { throw ... } }`);
+      assert.throws(() => url[name] = sym,
+                    /^TypeError: Cannot convert a Symbol value to a string$/,
+                    `url.${name} = ${String(sym)}`);
+    }
+  }
+}
diff --git a/test/parallel/test-whatwg-url-toascii.js b/test/parallel/test-whatwg-url-toascii.js
new file mode 100644
index 00000000000000..c85b092c1d250c
--- /dev/null
+++ b/test/parallel/test-whatwg-url-toascii.js
@@ -0,0 +1,85 @@
+'use strict';
+const common = require('../common');
+if (!common.hasIntl) {
+  // A handful of the tests fail when ICU is not included.
+  common.skip('missing Intl');
+}
+
+const fixtures = require('../common/fixtures');
+const { URL } = require('url');
+const { test, assert_equals, assert_throws } = require('../common/wpt');
+
+const request = {
+  response: require(fixtures.path('url-toascii'))
+};
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/4839a0a804/url/toascii.window.js
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+// async_test(t => {
+//   const request = new XMLHttpRequest()
+//   request.open("GET", "toascii.json")
+//   request.send()
+//   request.responseType = "json"
+//   request.onload = t.step_func_done(() => {
+    runTests(request.response)
+//   })
+// }, "Loading data…")
+
+function makeURL(type, input) {
+  input = "https://" + input + "/x"
+  if(type === "url") {
+    return new URL(input)
+  } else {
+    const url = document.createElement(type)
+    url.href = input
+    return url
+  }
+}
+
+function runTests(tests) {
+  for(var i = 0, l = tests.length; i < l; i++) {
+    let hostTest = tests[i]
+    if (typeof hostTest === "string") {
+      continue // skip comments
+    }
+    const typeName = { "url": "URL", "a": "<a>", "area": "<area>" }
+    // ;["url", "a", "area"].forEach((type) => {
+    ;["url"].forEach((type) => {
+      test(() => {
+        if(hostTest.output !== null) {
+          const url = makeURL("url", hostTest.input)
+          assert_equals(url.host, hostTest.output)
+          assert_equals(url.hostname, hostTest.output)
+          assert_equals(url.pathname, "/x")
+          assert_equals(url.href, "https://" + hostTest.output + "/x")
+        } else {
+          if(type === "url") {
+            assert_throws(new TypeError, () => makeURL("url", hostTest.input))
+          } else {
+            const url = makeURL(type, hostTest.input)
+            assert_equals(url.host, "")
+            assert_equals(url.hostname, "")
+            assert_equals(url.pathname, "")
+            assert_equals(url.href, "https://" + hostTest.input + "/x")
+          }
+        }
+      }, hostTest.input + " (using " + typeName[type] + ")")
+      ;["host", "hostname"].forEach((val) => {
+        test(() => {
+          const url = makeURL(type, "x")
+          url[val] = hostTest.input
+          if(hostTest.output !== null) {
+            assert_equals(url[val], hostTest.output)
+          } else {
+            assert_equals(url[val], "x")
+          }
+        }, hostTest.input + " (using " + typeName[type] + "." + val + ")")
+      })
+    })
+  }
+}
+/* eslint-enable */
diff --git a/test/parallel/test-whatwg-url-tojson.js b/test/parallel/test-whatwg-url-tojson.js
new file mode 100644
index 00000000000000..8e9a30c7e017e4
--- /dev/null
+++ b/test/parallel/test-whatwg-url-tojson.js
@@ -0,0 +1,17 @@
+'use strict';
+
+require('../common');
+const URL = require('url').URL;
+const { test, assert_equals } = require('../common/wpt');
+
+/* The following tests are copied from WPT. Modifications to them should be
+   upstreamed first. Refs:
+   https://github.com/w3c/web-platform-tests/blob/02585db/url/url-tojson.html
+   License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
+*/
+/* eslint-disable */
+test(() => {
+  const a = new URL("https://example.com/")
+  assert_equals(JSON.stringify(a), "\"https://example.com/\"")
+})
+/* eslint-enable */
diff --git a/test/parallel/test-whatwg-url-tostringtag.js b/test/parallel/test-whatwg-url-tostringtag.js
new file mode 100644
index 00000000000000..689056fd238dda
--- /dev/null
+++ b/test/parallel/test-whatwg-url-tostringtag.js
@@ -0,0 +1,32 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const URL = require('url').URL;
+
+// Tests below are not from WPT.
+const toString = Object.prototype.toString;
+
+const url = new URL('http://example.org');
+const sp = url.searchParams;
+const spIterator = sp.entries();
+
+const test = [
+  [url, 'URL'],
+  [sp, 'URLSearchParams'],
+  [spIterator, 'URLSearchParamsIterator'],
+  // Web IDL spec says we have to return 'URLPrototype', but it is too
+  // expensive to implement; therefore, use Chrome's behavior for now, until
+  // spec is changed.
+  [Object.getPrototypeOf(url), 'URL'],
+  [Object.getPrototypeOf(sp), 'URLSearchParams'],
+  [Object.getPrototypeOf(spIterator), 'URLSearchParamsIterator'],
+];
+
+test.forEach(([obj, expected]) => {
+  assert.strictEqual(obj[Symbol.toStringTag], expected,
+                     `${obj[Symbol.toStringTag]} !== ${expected}`);
+  const str = toString.call(obj);
+  assert.strictEqual(str, `[object ${expected}]`,
+                     `${str} !== [object ${expected}]`);
+});
diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js
index c2bca80641152e..ee50d48f61d45e 100644
--- a/tools/doc/type-parser.js
+++ b/tools/doc/type-parser.js
@@ -38,6 +38,8 @@ const typeMap = {
   'http.IncomingMessage': 'http.html#http_class_http_incomingmessage',
   'http.Server': 'http.html#http_class_http_server',
   'http.ServerResponse': 'http.html#http_class_http_serverresponse',
+  'URL': 'url.html#url_the_whatwg_url_api',
+  'URLSearchParams': 'url.html#url_class_urlsearchparams'
 };
 
 const arrayPart = /(?:\[])+$/;