Repository: electron/electron (language: cpp)

Issue:
[Bug]: ReadableStream is malformed when protocol.handle'd
### Preflight Checklist

- [X] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/main/CONTRIBUTING.md) for this project.
- [X] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/main/CODE_OF_CONDUCT.md) that this project adheres to.
- [X] I have searched the [issue tracker](https://www.github.com/electron/electron/issues) for a bug report that matches the one I want to file, without success.

### Electron Version

30.x, 29.x

### What operating system are you using?

macOS

### Operating System Version

macOS 14.4.1

### What arch are you using?

arm64 (including Apple Silicon)

### Last Known Working Electron version

_No response_

### Expected Behavior

1. [main] Define a custom protocol

```js
protocol.handle("myscheme", async (request) => {
    try {  await fs.rm("/tmp/myscheme.txt")   } catch {}
    for await (const p of request.body) {
        await fs.appendFile("/tmp/myscheme.txt", p)
    }
    return new Response("", { status: 200 })
})
```

2. [renderer] Make a request with the request body as a stream using standard web fetch + duplex "half"

```js
const dataStream = () =>
    new ReadableStream({
        async start(controller) {
            for (let i = 0; i < 10; i++) {
                // await wait(1000)
                controller.enqueue(Array(1024 * 128).fill(+i).join("\n"))
            }
            controller.close()
        },
    }).pipeThrough(new TextEncoderStream())

fetch(new Request("myscheme://host", {
    method: "POST",
    body: dataStream(),
    duplex: "half",
}))
```

3. [main] Verify the data (e.g. using shasum)


Expectation: The stream should come through intact.

### Actual Behavior

The stream gets malformed.

In the attached fiddle gist, you can see two buttons. 
- **Send** button sends the entire payload in the request body
- **Stream** button streams the payload in the request body (to pass a stream as the request body, we also need to set "duplex" to "half")

**Send** always works.
```
$ shasum /tmp/myscheme.txt
d269175ac71ed60bb96f08881c39453c8e8d7020  /tmp/myscheme.txt
```

**Stream** doesn't work. The resulting data (as evidenced by seeing its shasum) is different each time.

### Testcase Gist URL

https://gist.github.com/mnvr/e08d9f4876fb8400b7615347b4d268eb

### Additional Information

I hope I'm just missing something silly, but I tried to boil down the issue to a isolated gist, and I'm out of ideas about what to try next. I've tried with both the latest beta and the older stable 29.x.

FWIW, if the data is less than one chunk, the streaming seems to work fine (but then I guess it's not really streaming).

The gist also has a commented out await in the code that enqueues data to the stream. If we uncomment this, Stream starts to work - by adding a 1 second delay between these enqueues. But I'm not sure about this, smaller delays didn't work, and it is also possible that I've just been getting lucky with these 1 second delays.


Discussion / hints:

Fix patch:
diff --git a/lib/browser/api/protocol.ts b/lib/browser/api/protocol.ts
index 0bdec72983327..1b7f00b433c07 100644
--- a/lib/browser/api/protocol.ts
+++ b/lib/browser/api/protocol.ts
@@ -18,7 +18,7 @@ function makeStreamFromPipe (pipe: any): ReadableStream {
       try {
         const rv = await pipe.read(buf);
         if (rv > 0) {
-          controller.enqueue(buf.subarray(0, rv));
+          controller.enqueue(buf.slice(0, rv));
         } else {
           controller.close();
         }
diff --git a/spec/api-protocol-spec.ts b/spec/api-protocol-spec.ts
index 348f6c48e6b76..122cc4e84f0da 100644
--- a/spec/api-protocol-spec.ts
+++ b/spec/api-protocol-spec.ts
@@ -1381,6 +1381,49 @@ describe('protocol module', () => {
       expect(body).to.equal(text);
     });
 
+    it('can receive stream request body asynchronously', async () => {
+      let done: any;
+      const requestReceived: Promise<Buffer[]> = new Promise(resolve => { done = resolve; });
+      protocol.handle('http-like', async (req) => {
+        const chunks = [];
+        for await (const chunk of (req.body as any)) {
+          chunks.push(chunk);
+        }
+        done(chunks);
+        return new Response('ok');
+      });
+      defer(() => { protocol.unhandle('http-like'); });
+      const w = new BrowserWindow({ show: false });
+      w.loadURL('about:blank');
+      const expectedHashChunks = await w.webContents.executeJavaScript(`
+        const dataStream = () =>
+          new ReadableStream({
+            async start(controller) {
+              for (let i = 0; i < 10; i++) { controller.enqueue(Array(1024 * 128).fill(+i).join("\\n")); }
+              controller.close();
+            },
+          }).pipeThrough(new TextEncoderStream());
+        fetch(
+          new Request("http-like://host", {
+            method: "POST",
+            body: dataStream(),
+            duplex: "half",
+          })
+        );
+        (async () => {
+          const chunks = []
+          for await (const chunk of dataStream()) {
+            chunks.push(chunk);
+          }
+          return chunks;
+        })()
+      `);
+      const expectedHash = Buffer.from(await crypto.subtle.digest('SHA-256', Buffer.concat(expectedHashChunks))).toString('hex');
+      const body = Buffer.concat(await requestReceived);
+      const actualHash = Buffer.from(await crypto.subtle.digest('SHA-256', Buffer.from(body))).toString('hex');
+      expect(actualHash).to.equal(expectedHash);
+    });
+
     it('can receive multi-part postData from loadURL', async () => {
       protocol.handle('test-scheme', (req) => new Response(req.body));
       defer(() => { protocol.unhandle('test-scheme'); });
diff --git a/spec/index.js b/spec/index.js
index 259bca132c0f1..2082ea9b2a390 100644
--- a/spec/index.js
+++ b/spec/index.js
@@ -40,6 +40,7 @@ protocol.registerSchemesAsPrivileged([
   { scheme: global.standardScheme, privileges: { standard: true, secure: true, stream: false } },
   { scheme: global.zoomScheme, privileges: { standard: true, secure: true } },
   { scheme: global.serviceWorkerScheme, privileges: { allowServiceWorkers: true, standard: true, secure: true } },
+  { scheme: 'http-like', privileges: { standard: true, secure: true, corsEnabled: true, supportFetchAPI: true } },
   { scheme: 'cors-blob', privileges: { corsEnabled: true, supportFetchAPI: true } },
   { scheme: 'cors', privileges: { corsEnabled: true, supportFetchAPI: true } },
   { scheme: 'no-cors', privileges: { supportFetchAPI: true } },


Test patch: