Repository: wheresrhys/fetch-mock (js)

Issue:
"Body is unusable: Body has already been read" on multiple `fetch` calls when `response` parameter is a `Response` instance
```js
import fetchMock from "fetch-mock";

fetchMock
  .mockGlobal()
  .get(
    "https://www.example.com/",
    new Response(JSON.stringify({ name: "John Doe" }))
  );

const makeRequest = async () => {
  const response = await fetch("https://www.example.com/");
  const data = await response.json();
  console.log(data);
};

makeRequest(); // Ok
makeRequest(); // Error: Body is unusable: Body has already been read
```

Presumably the `Request` instance is being returned unmodified. However, the behavior is a bit confusing, and it might make sense to [`clone`](https://developer.mozilla.org/en-US/docs/Web/API/Response/clone) the `Request` instance before returning it to the `fetch` caller.

Workaround is to pass a [Response config](https://www.wheresrhys.co.uk/fetch-mock/docs/API/route/response#response-config), [Promise](https://www.wheresrhys.co.uk/fetch-mock/docs/API/route/response#promise), or [Function](https://www.wheresrhys.co.uk/fetch-mock/docs/API/route/response#function) as the `response` parameter, as these can create a unique `Request` instance per mocked `fetch` call.

Node v22.11.0

Pull request:
Rhys/reusable response
Fixes #908 

Patch:
diff --git a/packages/fetch-mock/src/Router.ts b/packages/fetch-mock/src/Router.ts
index bd726b5..eb335a6 100644
--- a/packages/fetch-mock/src/Router.ts
+++ b/packages/fetch-mock/src/Router.ts
@@ -226,7 +226,7 @@ export default class Router {
 		// If the response is a pre-made Response, respond with it
 		if (responseInput instanceof Response) {
 			return {
-				response: responseInput,
+				response: responseInput.clone(),
 				responseOptions: {},
 				responseInput: {},
 			};


Test patch:
diff --git a/packages/fetch-mock/src/__tests__/FetchMock/response-negotiation.test.js b/packages/fetch-mock/src/__tests__/FetchMock/response-negotiation.test.js
index 9e1fbc6..e2013f8 100644
--- a/packages/fetch-mock/src/__tests__/FetchMock/response-negotiation.test.js
+++ b/packages/fetch-mock/src/__tests__/FetchMock/response-negotiation.test.js
@@ -184,34 +184,44 @@ describe('response negotiation', () => {
 			);
 		});
 	});
-	it('Response', async () => {
-		fm.route('http://a.com/', new Response('http://a.com/', { status: 200 }));
-		const res = await fm.fetchHandler('http://a.com/');
-		expect(res.status).toEqual(200);
-	});
+	describe('Response', () => {
+		it('Response', async () => {
+			fm.route('http://a.com/', new Response('http://a.com/', { status: 200 }));
+			const res = await fm.fetchHandler('http://a.com/');
+			expect(res.status).toEqual(200);
+		});
 
-	it('should work with Response.error()', async () => {
-		fm.route('http://a.com', Response.error());
-		const response = await fm.fetchHandler('http://a.com');
-		expect(response.status).toBe(0);
-	});
+		it('should work with Response.error()', async () => {
+			fm.route('http://a.com', Response.error());
+			const response = await fm.fetchHandler('http://a.com');
+			expect(response.status).toBe(0);
+		});
 
-	it('function that returns a Response', async () => {
-		fm.route(
-			'http://a.com/',
-			() => new Response('http://a.com/', { status: 200 }),
-		);
-		const res = await fm.fetchHandler('http://a.com/');
-		expect(res.status).toEqual(200);
-	});
+		it('function that returns a Response', async () => {
+			fm.route(
+				'http://a.com/',
+				() => new Response('http://a.com/', { status: 200 }),
+			);
+			const res = await fm.fetchHandler('http://a.com/');
+			expect(res.status).toEqual(200);
+		});
 
-	it('Promise that returns a Response', async () => {
-		fm.route(
-			'http://a.com/',
-			Promise.resolve(new Response('http://a.com/', { status: 200 })),
-		);
-		const res = await fm.fetchHandler('http://a.com/');
-		expect(res.status).toEqual(200);
+		it('Promise that returns a Response', async () => {
+			fm.route(
+				'http://a.com/',
+				Promise.resolve(new Response('http://a.com/', { status: 200 })),
+			);
+			const res = await fm.fetchHandler('http://a.com/');
+			expect(res.status).toEqual(200);
+		});
+
+		it('Reuse a Response', async () => {
+			fm.route('http://a.com/', new Response('hello', { status: 200 }));
+			await fm.fetchHandler('http://a.com/').then((res) => res.text());
+			await expect(
+				fm.fetchHandler('http://a.com/').then((res) => res.text()),
+			).resolves.toEqual('hello');
+		});
 	});
 
 	describe('rejecting', () => {