Repository: nodejs/node
Pull request: url: add err.input to ERR_INVALID_FILE_URL_PATH

Otherwise there's no information from the error about what exactly is the invalid URL.

Background: I was debugging https://ci.nodejs.org/job/node-test-binary-windows-js-suites/36647/ and I had no idea why the tests are failing since there's no hint about what the URLs are after they get processed in the loader.

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: doc/api/errors.md
patch: @@ -1996,6 +1996,9 @@ A Node.js API that consumes `file:` URLs (such as certain functions in the
 [`fs`][] module) encountered a file URL with an incompatible path. The exact
 semantics for determining whether a path can be used is platform-dependent.
 
+The thrown error object includes an `input` property that contains the URL object
+of the invalid `file:` URL.
+
 <a id="ERR_INVALID_HANDLE_TYPE"></a>
 
 ### `ERR_INVALID_HANDLE_TYPE`

filename: lib/internal/errors.js
patch: @@ -1489,7 +1489,10 @@ E('ERR_INVALID_FD',
 E('ERR_INVALID_FD_TYPE', 'Unsupported fd type: %s', TypeError);
 E('ERR_INVALID_FILE_URL_HOST',
   'File URL host must be "localhost" or empty on %s', TypeError);
-E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s', TypeError);
+E('ERR_INVALID_FILE_URL_PATH', function(reason, input) {
+  this.input = input;
+  return `File URL path ${reason}`;
+}, TypeError);
 E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError);
 E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError, HideStackFramesError);
 E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError);

filename: lib/internal/url.js
patch: @@ -1463,7 +1463,7 @@ function getPathFromURLWin32(url) {
       if ((pathname[n + 1] === '2' && third === 102) || // 2f 2F /
           (pathname[n + 1] === '5' && third === 99)) {  // 5c 5C \
         throw new ERR_INVALID_FILE_URL_PATH(
-          'must not include encoded \\ or / characters',
+          'must not include encoded \\ or / characters', url,
         );
       }
     }
@@ -1484,7 +1484,7 @@ function getPathFromURLWin32(url) {
   const sep = StringPrototypeCharAt(pathname, 2);
   if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z ||   // a..z A..Z
       (sep !== ':')) {
-    throw new ERR_INVALID_FILE_URL_PATH('must be absolute');
+    throw new ERR_INVALID_FILE_URL_PATH('must be absolute', url);
   }
   return StringPrototypeSlice(pathname, 1);
 }
@@ -1551,7 +1551,7 @@ function getPathBufferFromURLWin32(url) {
 
   if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z ||   // a..z A..Z
       (sep !== CHAR_COLON)) {
-    throw new ERR_INVALID_FILE_URL_PATH('must be absolute');
+    throw new ERR_INVALID_FILE_URL_PATH('must be absolute', url);
   }
 
   // Now, we'll just return everything except the first byte of
@@ -1570,6 +1570,7 @@ function getPathFromURLPosix(url) {
       if (pathname[n + 1] === '2' && third === 102) {
         throw new ERR_INVALID_FILE_URL_PATH(
           'must not include encoded / characters',
+          url,
         );
       }
     }

filename: test/parallel/test-url-fileurltopath.js
patch: @@ -31,24 +31,6 @@ test('fileURLToPath with host', () => {
   }
 });
 
-test('fileURLToPath with invalid path', () => {
-  if (isWindows) {
-    assert.throws(() => url.fileURLToPath('file:///C:/a%2F/'), {
-      code: 'ERR_INVALID_FILE_URL_PATH'
-    });
-    assert.throws(() => url.fileURLToPath('file:///C:/a%5C/'), {
-      code: 'ERR_INVALID_FILE_URL_PATH'
-    });
-    assert.throws(() => url.fileURLToPath('file:///?:/'), {
-      code: 'ERR_INVALID_FILE_URL_PATH'
-    });
-  } else {
-    assert.throws(() => url.fileURLToPath('file:///a%2F/'), {
-      code: 'ERR_INVALID_FILE_URL_PATH'
-    });
-  }
-});
-
 const windowsTestCases = [
   // Lowercase ascii alpha
   { path: 'C:\\foo', fileURL: 'file:///C:/foo' },

filename: test/parallel/test-url-invalid-file-url-path-input.js
patch: @@ -0,0 +1,25 @@
+'use strict';
+
+// This tests that url.fileURLToPath() throws ERR_INVALID_FILE_URL_PATH
+// for invalid file URL paths along with the input property.
+
+const { isWindows } = require('../common');
+const assert = require('assert');
+const url = require('url');
+
+const inputs = [];
+
+if (isWindows) {
+  inputs.push('file:///C:/a%2F/', 'file:///C:/a%5C/', 'file:///?:/');
+} else {
+  inputs.push('file:///a%2F/');
+}
+
+for (const input of inputs) {
+  assert.throws(() => url.fileURLToPath(input), (err) => {
+    assert.strictEqual(err.code, 'ERR_INVALID_FILE_URL_PATH');
+    assert(err.input instanceof URL);
+    assert.strictEqual(err.input.href, input);
+    return true;
+  });
+}