Repository: node-fetch/node-fetch (js)

Issue:
Handling a BOM with .json()
Anybody know how I should be handling a BOM with a returned json payload? Right now i'm getting this error when calling res.json()
`Exception: FetchError: invalid json response body at https://api_that_returns_json_with_bom reason: Unexpected token ? in JSON at position 0`

I tried adding an Accept header of `application/json; charset=utf-8` but that didn't resolve the issue.

Pull request:
fix: handle bom in text and json
<!-- Thanks for contributing! -->

## Purpose
long over due problem to an easy fix... `.text()` & `.json()` didn't handle BOM correctly

## Changes
switch `buffer.toString()` to `TextDecoder().decode()`

___

<!-- Mark the ones you have done and remove unnecessary ones. Add new tasks that fit (like TODOs). -->
- [x] I added unit test(s)

___

<!-- Add `- fix #_NUMBER_` line for every PR/Issue this PR solves. Do not comma separate them. -->
- fix #541 


Patch:
diff --git a/@types/index.d.ts b/@types/index.d.ts
index c0e78bc..2a482f6 100644
--- a/@types/index.d.ts
+++ b/@types/index.d.ts
@@ -1,7 +1,8 @@
 /// <reference types="node" />
 /// <reference lib="dom" />
 
-import {Agent} from 'http';
+import {RequestOptions} from 'http';
+import {FormData} from 'formdata-polyfill/esm.min.js';
 import {
 	Blob,
 	blobFrom,
@@ -21,6 +22,7 @@ type AbortSignal = {
 export type HeadersInit = Headers | Record<string, string> | Iterable<readonly [string, string]> | Iterable<Iterable<string>>;
 
 export {
+	FormData,
 	Blob,
 	blobFrom,
 	blobFromSync,
@@ -98,7 +100,7 @@ export interface RequestInit {
 	referrerPolicy?: ReferrerPolicy;
 
 	// Node-fetch extensions to the whatwg/fetch spec
-	agent?: Agent | ((parsedUrl: URL) => Agent);
+	agent?: RequestOptions['agent'] | ((parsedUrl: URL) => RequestOptions['agent']);
 	compress?: boolean;
 	counter?: number;
 	follow?: number;
diff --git a/README.md b/README.md
index a12ad28..6b27b2a 100644
--- a/README.md
+++ b/README.md
@@ -171,7 +171,7 @@ Using an old version of node-fetch? Check out the following files:
 
 - [2.x to 3.x upgrade guide](docs/v3-UPGRADE-GUIDE.md)
 - [1.x to 2.x upgrade guide](docs/v2-UPGRADE-GUIDE.md)
-- [Changelog](docs/CHANGELOG.md)
+- [Changelog](https://github.com/node-fetch/node-fetch/releases)
 
 ## Common Usage
 
@@ -423,7 +423,7 @@ node-fetch comes with a spec-compliant [FormData] implementations for posting
 multipart/form-data payloads
 
 ```js
-import fetch {FormData, File, fileFrom} from 'node-fetch'
+import fetch, { FormData, File, fileFrom } from 'node-fetch'
 
 const httpbin = 'https://httpbin.org/post'
 const formData = new FormData()
@@ -470,7 +470,7 @@ You may cancel requests with `AbortController`. A suggested implementation is [`
 An example of timing out a request after 150ms could be achieved as the following:
 
 ```js
-import fetch from 'node-fetch';
+import fetch, { AbortError } from 'node-fetch';
 
 // AbortController was added in node v14.17.0 globally
 const AbortController = globalThis.AbortController || await import('abort-controller')
@@ -484,7 +484,7 @@ try {
 	const response = await fetch('https://example.com', {signal: controller.signal});
 	const data = await response.json();
 } catch (error) {
-	if (error instanceof fetch.AbortError) {
+	if (error instanceof AbortError) {
 		console.log('request was aborted');
 	}
 } finally {
@@ -868,4 +868,4 @@ Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid
 [error-handling.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md
 [FormData]: https://developer.mozilla.org/en-US/docs/Web/API/FormData
 [Blob]: https://developer.mozilla.org/en-US/docs/Web/API/Blob
-[File]: https://developer.mozilla.org/en-US/docs/Web/API/File
\ No newline at end of file
+[File]: https://developer.mozilla.org/en-US/docs/Web/API/File
diff --git a/src/body.js b/src/body.js
index b0fe16b..714e27e 100644
--- a/src/body.js
+++ b/src/body.js
@@ -145,8 +145,8 @@ export default class Body {
 	 * @return  Promise
 	 */
 	async json() {
-		const buffer = await consumeBody(this);
-		return JSON.parse(buffer.toString());
+		const text = await this.text();
+		return JSON.parse(text);
 	}
 
 	/**
@@ -156,7 +156,7 @@ export default class Body {
 	 */
 	async text() {
 		const buffer = await consumeBody(this);
-		return buffer.toString();
+		return new TextDecoder().decode(buffer);
 	}
 
 	/**
diff --git a/src/request.js b/src/request.js
index 76d7576..a4d1b22 100644
--- a/src/request.js
+++ b/src/request.js
@@ -57,7 +57,7 @@ export default class Request extends Body {
 		}
 
 		if (parsedURL.username !== '' || parsedURL.password !== '') {
-			throw new TypeError(`${parsedURL} is an url with embedded credentails.`);
+			throw new TypeError(`${parsedURL} is an url with embedded credentials.`);
 		}
 
 		let method = init.method || input.method || 'GET';


Test patch:
diff --git a/@types/index.test-d.ts b/@types/index.test-d.ts
index 4b24dcb..6277665 100644
--- a/@types/index.test-d.ts
+++ b/@types/index.test-d.ts
@@ -78,6 +78,8 @@ async function run() {
 	expectAssignable<Body>(request);
 
 	/* eslint-disable no-new */
+	new Request('url', {agent: false});
+
 	new Headers({Header: 'value'});
 	// new Headers(['header', 'value']); // should not work
 	new Headers([['header', 'value']]);
diff --git a/test/response.js b/test/response.js
index 714cda1..b515762 100644
--- a/test/response.js
+++ b/test/response.js
@@ -77,6 +77,21 @@ describe('Response', () => {
 		expect(res.headers.get('a')).to.equal('1');
 	});
 
+	it('should decode responses containing BOM to json', async () => {
+		const json = await new Response('\uFEFF{"a":1}').json();
+		expect(json.a).to.equal(1);
+	});
+
+	it('should decode responses containing BOM to text', async () => {
+		const text = await new Response('\uFEFF{"a":1}').text();
+		expect(text).to.equal('{"a":1}');
+	});
+
+	it('should keep BOM when getting raw bytes', async () => {
+		const ab = await new Response('\uFEFF{"a":1}').arrayBuffer();
+		expect(ab.byteLength).to.equal(10);
+	});
+
 	it('should support text() method', () => {
 		const res = new Response('a=1');
 		return res.text().then(result => {