Repository: sindresorhus/query-string
Pull request #355 (2022-12-04 11:29:27)

fix: Encode fragmentIdentifier with encodeURI method not encodeURIComponent at stringifyUrl
Fixes: #346 

FragmentIdentifier should be encoded with encodeURI method not encodeURIComponent method.
Also added test code about this fix.

sindresorhus: Are you sure it's correct to use `encodeURI`? AFAIK, it's meant for complete URLs. Can you link to a source about it being correct to use in this situation?
hanbin9775: I did some investigation, and found out something wrong about my PR. Thank you for the feedback!

### URL hash setter in Node.js

Node.js implements url.hash setter with [this following. ](https://nodejs.org/api/url.html#urlhash)

> Invalid URL characters included in the value assigned to the hash property are [percent-encoded](https://nodejs.org/api/url.html#percent-encoding-in-urls).

Url fragments are percent encoded with following percent-encode set. (As known as `fragment percent-encode set`)

> C0 control percent-encode set and code points U+0020, U+0022, U+003C, U+003E, and U+0060.

C0 control percent-encode set is

> code points in range U+0000 to U+001F (inclusive) and all code points greater than U+007E.

You can find [list of code points in here.](https://www.obliquity.com/computer/html/unicode0000.html)

This is also described in URL Standard. (https://url.spec.whatwg.org/#fragment-state)

### encodeURI, encodeURIComponent

The only difference with encodeURI and encodeURIComponent is whether the following character set is encoded: ` ;/?:@&=+$,#` 
(https://tc39.es/ecma262/multipage/global-object.html#sec-encode)

Both methods encodes characters which is **not** [the ASCII word characters](https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#ASCII-word-characters) and "-.!~*'()".

### Conclusion: difference with encodeURI(Component) and url.hash setter

Found out percent-encode sets are different.
If we express the character set that will display different encoding result, it will be as follows
* `Universal Character Set` - (`fragment percent-encode set` + `the ASCII word characters set` + `-.!~*'()`)

For example code point U+005B ( `[` ) is not included on `fragment percent-encode set` but url.hash setter will percent-encode it. 

```
const fragement = '[';
const x = new URL(url);
x.hash = fragment;

console.log(encodeURI(fragment)); // '%5B'
console.log(encodeURIComponent(fragment)); // '%5B'

console.log(url.hash); // '['
```

So I believe the easiest implement for fixing #355 issue will be like this.

as-is
```
if (object.fragmentIdentifier) {
	hash = `#${options[encodeFragmentIdentifier] ? encode(object.fragmentIdentifier, options) : object.fragmentIdentifier}`;
}
```
to-be
```
if (object.fragmentIdentifier) {
	const newURL = new URL(url);
	newURL.hash = object.fragmentIdentifier;
	hash = options[encodeFragmentIdentifier] ? newURL.hash : `#${object.fragmentIdentifier}`;
}
```

Or we could override encodeURIComponent function for encoding url fragments. `encodeURIFragments`

Patch:
diff --git a/index.js b/index.js
index d45e67d..129ea26 100644
--- a/index.js
+++ b/index.js
@@ -455,7 +455,9 @@ exports.stringifyUrl = (object, options) => {
 
 	let hash = getHash(object.url);
 	if (object.fragmentIdentifier) {
-		hash = `#${options[encodeFragmentIdentifier] ? encode(object.fragmentIdentifier, options) : object.fragmentIdentifier}`;
+		const urlObjectForFragmentEncode = new URL(url);
+		urlObjectForFragmentEncode.hash = object.fragmentIdentifier;
+		hash = options[encodeFragmentIdentifier] ? urlObjectForFragmentEncode.hash : `#${object.fragmentIdentifier}`;
 	}
 
 	return `${url}${queryString}${hash}`;


Test patch:
diff --git a/test/stringify-url.js b/test/stringify-url.js
index 2b3c372..9b6ff43 100644
--- a/test/stringify-url.js
+++ b/test/stringify-url.js
@@ -26,6 +26,7 @@ test('stringify URL with fragment identifier', t => {
 	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar/#abc', query: {}, fragmentIdentifier: 'top'}), 'https://foo.bar/#top');
 	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {}}), 'https://foo.bar');
 	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {}, fragmentIdentifier: 'foo bar'}), 'https://foo.bar#foo%20bar');
+	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar/', query: {}, fragmentIdentifier: '/foo/bar'}), 'https://foo.bar/#/foo/bar');
 });
 
 test('skipEmptyString:: stringify URL with a query string', t => {