Skip to content

js/qmux: Session logs console.warn("abort", ...) on every application-initiated writer.abort() #344

Description

@fperex

Summary

Session calls console.warn("abort", e) from all three of its WritableStream sinks. A WritableStream sink's abort runs for any writer.abort(reason), including the ordinary application-initiated teardown of a stream, so this fires on healthy sessions and there is no way for a consumer to turn it off.

Present at HEAD in js/qmux/src/session.ts at lines 1375, 1683, 1763, and in the published 0.3.0 and 0.3.1. It has been there since the crate was added (a4786af, #191), so it reads like a debug leftover rather than a deliberate diagnostic.

abort: (e) => {
    console.warn("abort", e);
    this.#scheduler?.dropStream(streamId, e instanceof Error ? e : new Error("stream aborted"));
    this.#sendPriorityFrame({ type: "reset_stream", id: frame.id, code: VarInt.from(0), ... });
    ...
},

The three sites:

Line Stream
1375 incoming (peer-created) bidi, send half
1683 createBidirectionalStream, send half
1763 createUnidirectionalStream

Why it matters in practice

moq's js/net routes Safari and Firefox onto qmux (js/net/src/connection/browser.ts disables WebTransport for both), and its stream teardown is Writer.reset(reason) calling writer.abort(reason). So every abort in normal operation prints a line into the user's console. The same teardown on Chrome, over native WebTransport, is silent, which makes it look browser-specific to whoever is reading the log.

A real example, from a Safari publisher against a local relay. Every one of these is qmux, none is the application:

[warn] abort Error: unexpected end of stream
#fillTo@http://localhost:5174/@fs/.../js/net/src/stream.ts:89:20
[warn] abort Error: unexpected end of stream
[warn] abort Error: unexpected end of stream
[warn] abort Error: RESET_STREAM: 0
#handleResetStream@.../@moq_qmux.js:2079:39
#recvFrame@.../@moq_qmux.js:1769:66
#onData@.../@moq_qmux.js:1753:48
#readLoop@.../@moq_qmux.js:1715:17

Two things make this expensive to be on the receiving end of:

  1. The message is the bare word abort with no stream id, no direction, and no indication of which library emitted it. The only clue is the Error argument's own stack, which points at wherever the error was constructed, not at what aborted. In the example above that is a moq source file, so the natural reading is "moq bug", and qmux never appears in the line at all.
  2. It is console.warn, so it survives production builds and shows up in end users' consoles and in any log capture the application does.

For what it is worth, tracking that first line down to qmux and then to a stream-resurrection race took a full instrumented A/B. Adding the stream id and direction to the message would have collapsed most of that. If the line stays in any form, something like `qmux: stream ${streamId} (${dir}) aborted by the application` would be a large improvement over abort. The neighbouring diagnostics in the same file already do this (qmux: keep-alive ping failed, qmux: control backlog ... exceeds high-water).

Repro

Nothing exotic is needed. Any application-initiated abort triggers it:

import Session from "@moq/qmux";

const session = new Session(url, { protocols: [...] });
await session.ready;

const { writable } = await session.createBidirectionalStream();
const writer = writable.getWriter();
await writer.write(new Uint8Array([1]));
await writer.abort(new Error("done with this stream")).catch(() => {});
// console: abort Error: done with this stream

Note the abort here is a deliberate, correct call by the application. Nothing has gone wrong at the transport level.

Suggested direction

Not prescribing a shape, but the options as I see them:

  • Drop the three console.warn("abort", e) calls. An application-initiated abort is not an event the transport needs to narrate; the caller already knows, and the peer learns via RESET_STREAM.
  • Or keep a diagnostic but make it identify itself and the stream, and put it behind whatever debug switch the rest of the library would use, so it does not reach production consoles by default.

Either way it would be worth a quick grep for other bare console.* calls on hot paths at the same time. session.ts currently also has console.error("Protocol violation:", error) and console.warn("serverCertificateHashes is not supported; trying anyway"), which are arguably fine since they are genuine faults, but they have the same "library writes to the app's console" property.

Environment

  • @moq/qmux 0.3.0 and 0.3.1, and js/qmux/src/session.ts at moq-dev/web-transport HEAD (6176697)
  • Reproduced on Safari 26.5, Firefox, and Chromium (forced onto the WebSocket path), macOS 15.5 arm64
  • Consumer: moq-dev/moq js/net at dev ee4a0b49

Unrelated to this report, and mentioned only so it does not get filed twice: the two 0.3.0 bugs this investigation started from (a cancelled stream id being resurrected by an in-flight FIN, and #readLoop not normalizing Chromium's native WebSocketStream ArrayBuffer chunks) are both already fixed on main and released in 0.3.1, via #306 and #324. Verified against both versions. The only thing still outstanding is the logging above.

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