Repository: nodejs/undici

Problem statement:
Change "Body is unusable" error to something more helpful
## This would solve...

Unidici throws an error with the message "Body is unusable" when the body of a response is non-null and the stream is either locked or disturbed. In most cases, this situation occurs because someone has attempted to ready the body twice, such as:

```js
const response = await fetch(url);
console.log(await response.text());
const body = await response.json();  // throws "Body is unusable"
```

The message "Body is unusable" is opaque and requires searching to understand what it means and what the problem is. A more descriptive message would save developers a lot of time.

## The implementation should look like...

Changing the string on this line: https://github.com/nodejs/undici/blob/fe44b9b36718ff2c171568fc4a239ecb8eba038d/lib/web/fetch/body.js#L411

Suggestion: "Body is unusable: Body is either null or the stream has already been read."

The spec only specifies that a `TypeError` must be thrown, but it does not specify the text contained within that error, so it seems like changing the message would still be in-line with the spec.

## I have also considered...

Not filing this issue. :smile:

## Additional context

<!-- Add any other context, screenshots or ideas about the feature request
here. -->



Hints:
Thanks for reporting! Would you like to send a Pull Request to address this issue? Remember to add unit tests.
I am not certain that the body can be null there
@mcollina happy to! :tada:
If I had to implement it, I would do it like this:

```js
// symbols.js
const kBodyOk = Symbol('body_ok')
const kBodyLocked = Symbol('body_locked')
const kBodyDisturbed = Symbol('body_disturbed')

// https://fetch.spec.whatwg.org/#body-unusable
function bodyUnusable (body) {
  // An object including the Body interface mixin is
  // said to be unusable if its body is non-null and
  // its body’s stream is disturbed or locked.
  if (body != null) {
    if (body.stream.locked) {
      return kBodyLocked
    } else if (util.isDisturbed(body.stream))
      return kBodyDisturbed
    }
  }
  return kBodyOk
}
```


then throw it properly in the linked code place. or directly throw in bodyUnusable. Maybe rename it to assertBodyUsable. 
But probably @KhafraDev has a strong opinion regarding such a change.
@KhafraDev ah gotcha, I see that [`bodyUsable()` returns `false` if body is `null`](https://github.com/nodejs/undici/blob/fe44b9b36718ff2c171568fc4a239ecb8eba038d/lib/web/fetch/body.js#L448-L453). 👍 So maybe just "Body is unusable: The stream has already been read."? 
That message isn't much better, it assumes that you have some knowledge of fetch internals (that the body mixin methods are helper methods for consuming .body, which is a ReadableStream).

This example doesn't use any streams outwardly:
```js
const response = await fetch('https://www.google.com')
res.text()
res.text() // TypeError: Body is unusable
```

I don't think "read" is the correct verbiage here either as it might imply that the entire body has been read, which also may not be true. Also, "read" is something you can do to a ReadableStream so it may confuse people who do not directly access .body.

```js
const response = await fetch('https://www.goolge.com')
const reader = response.body.getReader()

await reader.read() // a single chunk

reader.releaseLock()

await response.text() // TypeError: Body is unusable
```

Instead, it should be something along the lines of "Body has already been consumed". Still, I don't necessarily think that the message is entirely clear and I suspect people will still have to look up what it actually means. Suggestions are welcome.

Patch:
diff --git a/lib/web/fetch/body.js b/lib/web/fetch/body.js
--- a/lib/web/fetch/body.js
+++ b/lib/web/fetch/body.js
@@ -406,7 +406,7 @@ async function consumeBody (object, convertBytesToJSValue, instance) {
   // 1. If object is unusable, then return a promise rejected
   //    with a TypeError.
   if (bodyUnusable(object[kState].body)) {
-    throw new TypeError('Body is unusable')
+    throw new TypeError('Body is unusable: Body has already been read')
   }
 
   throwIfAborted(object[kState])


Test patch:
diff --git a/test/fetch/client-fetch.js b/test/fetch/client-fetch.js
--- a/test/fetch/client-fetch.js
+++ b/test/fetch/client-fetch.js
@@ -370,7 +370,7 @@ test('locked blob body', (t, done) => {
     const res = await fetch(`http://localhost:${server.address().port}`)
     const reader = res.body.getReader()
     res.blob().catch(err => {
-      strictEqual(err.message, 'Body is unusable')
+      strictEqual(err.message, 'Body is unusable: Body has already been read')
       reader.cancel()
     }).finally(done)
   })
@@ -390,7 +390,7 @@ test('disturbed blob body', (t, done) => {
       ok(true)
     })
     await res.blob().catch(err => {
-      strictEqual(err.message, 'Body is unusable')
+      strictEqual(err.message, 'Body is unusable: Body has already been read')
     })
     done()
   })
diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js
--- a/test/node-fetch/main.js
+++ b/test/node-fetch/main.js
@@ -1385,7 +1385,7 @@ describe('node-fetch', () => {
       assert.strictEqual(res.headers.get('content-type'), 'text/plain')
       return res.text().then(() => {
         assert.strictEqual(res.bodyUsed, true)
-        return assert.rejects(res.text(), new TypeError('Body is unusable'))
+        return assert.rejects(res.text(), new TypeError('Body is unusable: Body has already been read'))
       })
     })
   })