Skip to content

Writing new Uint8Array() causes server freeze with quic_bug_10586_6: span.empty() && !fin #365

Description

@guest271314

During testing.

Server. bun build --target=browser --packages=bundle ./node_modules/@fails-components/webtransport/lib/index.node.js --outfile=webtransport-bundle.js. Add node: specifiers where they don't exist in

import { Http3Server } from "./webtransport-bundle.js";
import certificates from "../cert.json" with { type: "json" };

let requests = 0;
const server = new Http3Server({
  port: 8080,
  host: "127.0.0.1",
  secret: certificates[0].secret,
  cert: certificates[0].pem,
  privKey: certificates[0].privateKey,
});

await server.startServer();
await server.ready;

console.info("server started");

const address = server.address();

const sessionStream = server.sessionStream("/");
const sessionReader = sessionStream.getReader();

while (true) {
  const { value: session, done } = await sessionReader.read();
  if (done) {
    break;
  }
  console.log(session.closed);
  let incomingTotalLength = 0;
  let incomingCurrentLength = 0;
  const buffer = new ArrayBuffer(0, { maxByteLength: 4 });
  const view = new DataView(buffer);
  const encoder = new TextEncoder();
  const decoder = new TextDecoder();
  for await (
    const { readable, writable } of session.incomingBidirectionalStreams
  ) {
    const writer = writable.getWriter();
    await readable
      .pipeTo(
        new WritableStream({
          async write(value) {
            if (incomingTotalLength === 0 && incomingCurrentLength === 0) {
              buffer.resize(4);
              for (let i = 0; i < 4; i++) {
                view.setUint8(i, value[i]);
              }
              incomingTotalLength = view.getUint32(0, true);
              console.log(value.length, incomingTotalLength);
              value = value.subarray(4);
            }
            const encoded = encoder.encode(
              decoder.decode(value).toUpperCase(),
            );
            await writer.ready;
            await writer.write(encoded);
            await writer.ready; //.then(() => console.log(`Done writing ${encoded.length} to writable, ${} total bytes written.`));
            incomingCurrentLength += encoded.length;
            console.log(
              `Done writing ${encoded.length} bytes to writable, ${incomingCurrentLength} of ${incomingTotalLength} bytes written.`,
            );
          },
          close() {
            console.log("readable closed");
          },
        }),
      ).then(() => console.log("pipeTo() done"))
      .catch((e) => console.log(e));

    buffer.resize(0);
    incomingTotalLength = 0;
    incomingCurrentLength = 0;
  }
  await session.closed.then(() => ({
    code: 5000,
    reason: `Done streaming request ${requests++}`,
  })).then((res) => console.log(res)).catch((e) => e.message);
}

Client (Chromium 137). One session, multiple streams. Starting with 7 MB. When done with session execute client.close({ closeCode: 5000, reason: "Done streaming" });

const serverCertificateHashes = [{
  "algorithm": "sha-256",
  "value": Uint8Array.of(1,2,3,4,... ), // Hard coded, once
}, ];
// Use Deno's WebTransport client in Deno, which doesn't support webtransport.node
if (!/Deno|Chrome|Firefox/i.test(navigator.userAgent)) {
  let {WebTransport, quicheLoaded} = await import("./node_modules/@fails-components/webtransport/lib/index.node.js");
  await quicheLoaded;
  globalThis.WebTransport = WebTransport;
}

async function createStream(wt, data) {
  const abortable = new AbortController();
  const {signal} = abortable;
  const {readable, writable} = await wt.createBidirectionalStream();
  let header = new Uint8Array(Uint32Array.from({
    length: 4,
  }, (_, index) => (data.length >> (index * 8)) & 0xff, ));
  let view = new DataView(header.buffer);
  let outgoingTotalLength = view.getUint32(0, true);
  console.log({
    outgoingTotalLength
  });
  let incomingTotalLength = 0;
  const writer = writable.getWriter();
  await writer.ready;
  await writer.write(header).then( () => console.log(`Outgoing total length ${outgoingTotalLength} written.`));
  await writer.ready;
  await writer.write(data).then( () => console.log(`${data.length} bytes written.`)).catch( (e) => console.log(e));
  await writer.ready;
  for await(const value of readable.pipeThrough(new TextDecoderStream())) {
    console.log(incomingTotalLength += value.length);
    if (incomingTotalLength === outgoingTotalLength) {
      await writer.ready;
      await writer.close().catch( (e) => {}
      );
      await writer.closed;
      writer.releaseLock();
      break;
    }
  }
  console.log(readable.locked);
  return "Session done";
}

const client = new WebTransport(`https://127.0.0.1:8080`,{
  serverCertificateHashes,
},);
client.closed.then(console.log).catch(console.log);
await client.ready;
const encoder = new TextEncoder();

var data = encoder.encode("x".repeat((1024 ** 2) * 7)); // 7MB

var res = await createStream(client, data); // "Session done"
// client.close({ closeCode: 5000, reason: "Done streaming" });
console.log(res);

Everything works as expected.

Image

Write an empty Uint8Array

var res = await createStream(client, new Uint8Array());
// client.close({ closeCode: 5000, reason: "Done streaming" });
console.log(res);

What happens in the server

E0418 03:33:32.101059   46669 quic_stream.cc:823] quic_bug_10586_6: span.empty() && !fin

Image

Thereafter the session is frozen.

Write 1 MB

var res = await createStream(client, new Uint8Array(1024**2));
// client.close({ closeCode: 5000, reason: "Done streaming" });
console.log(res);

Nothing happens in the server

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions