Skip to content

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

@guest271314

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions