Skip to content

Request() rejects an open ReadableStream, breaking Fetch capability detection #3163

Description

@turicas

A public Brazilian election-results page fails before it can load a candidate profile in Lightpanda. The page is an Angular application. While opening a candidate route, it loads one extra JavaScript file. That file runs a small feature check: it asks whether the browser accepts a ReadableStream as the body of a Request. Firefox accepts the request object, so the application continues. Lightpanda throws TypeError instead and Angular stops loading that part of the page.

This is reproducible without the TSE website (see Minimal independent reproduction). It appears to be caused by Lightpanda requiring a stream to be closed when Request is created. Web browsers accept an open stream at that point; the stream may produce data later.

Environment

Reproduction

lightpanda fetch \
  --log-level debug \
  --wait-ms 8000 \
  --dump semantic_tree_text \
  'https://divulgacandcontas.tse.jus.br/divulga/#/candidato/SUDESTE/ES/2045202024/80002363211/2024/56553'
Output with `TypeError` ``` $time=1786290795155 $scope=telemetry $level=info $msg="telemetry status" disabled=false $time=1786290795155 $scope=app $level=debug $msg="startup" mode=fetch dump_mode=semantic_tree_text url=https://divulgacandcontas.tse.jus.br/divulga/#/candidato/SUDESTE/ES/2045202024/80002363211/2024/56553 snapshot=true $time=1786290795161 $scope=frame $level=info $msg="navigate" url=https://divulgacandcontas.tse.jus.br/divulga/#/candidato/SUDESTE/ES/2045202024/80002363211/2024/56553 method=GET reason=address_bar body=false type=root $time=1786290795286 $scope=http $level=debug $msg="script fetch start" req=GET https://divulgacandcontas.tse.jus.br/divulga/runtime.113bd67c35d7d457.js $time=1786290795286 $scope=browser $level=info $msg="executing script" src=https://divulgacandcontas.tse.jus.br/divulga/runtime.113bd67c35d7d457.js kind=module $time=1786290795354 $scope=http $level=debug $msg="script fetch start" req=GET https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js $time=1786290795354 $scope=browser $level=info $msg="executing script" src=https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js kind=module $time=1786290795386 $scope=http $level=debug $msg="script fetch start" req=GET https://divulgacandcontas.tse.jus.br/divulga/scripts.46623e42e37f6d34.js $time=1786290795386 $scope=browser $level=info $msg="executing script" src=https://divulgacandcontas.tse.jus.br/divulga/scripts.46623e42e37f6d34.js kind=javascript $time=1786290795501 $scope=http $level=debug $msg="script fetch start" req=GET https://divulgacandcontas.tse.jus.br/divulga/main.764e646b5bc838bc.js $time=1786290795502 $scope=browser $level=info $msg="executing script" src=https://divulgacandcontas.tse.jus.br/divulga/main.764e646b5bc838bc.js kind=module $time=1786290795603 $scope=http $level=info $msg="request complete" source=xhr url=https://divulgacandcontas.tse.jus.br/divulga/rest/v1/sistema status=200 len=240 $time=1786290795607 $scope=http $level=info $msg="request complete" source=xhr url=https://divulgacandcontas.tse.jus.br/divulga/rest/v1/ata/ordinarias status=200 len=3628 $time=1786290795620 $scope=http $level=info $msg="request complete" source=xhr url=https://divulgacandcontas.tse.jus.br/divulga/rest/v1/eleicao/anos-eleitorais status=200 len=61 $time=1786290795662 $scope=http $level=debug $msg="script fetch start" req=GET https://divulgacandcontas.tse.jus.br/divulga/common.d23f7d2b9571464e.js $time=1786290795667 $scope=http $level=debug $msg="script fetch start" req=GET https://divulgacandcontas.tse.jus.br/divulga/829.e77b9db3d69bf20e.js $time=1786290795803 $scope=http $level=debug $msg="script fetch start" req=GET https://divulgacandcontas.tse.jus.br/divulga/684.398dd03bcb7e68ff.js $time=1786290795804 $scope=browser $level=info $msg="executing script" src=https://divulgacandcontas.tse.jus.br/divulga/684.398dd03bcb7e68ff.js kind=module $time=1786290795829 $scope=http $level=debug $msg="script fetch start" req=GET https://divulgacandcontas.tse.jus.br/divulga/636.85c158c04b59c757.js $time=1786290795829 $scope=browser $level=info $msg="executing script" src=https://divulgacandcontas.tse.jus.br/divulga/636.85c158c04b59c757.js kind=module $time=1786290795841 $scope=browser $level=info $msg="executing script" src=https://divulgacandcontas.tse.jus.br/divulga/common.d23f7d2b9571464e.js kind=module $time=1786290795841 $scope=browser $level=info $msg="executing script" src=https://divulgacandcontas.tse.jus.br/divulga/829.e77b9db3d69bf20e.js kind=module $time=1786290795871 $scope=js $level=warn $msg="console.error" param.0=Error: Uncaught (in promise): TypeError TypeError at https://divulgacandcontas.tse.jus.br/divulga/829.e77b9db3d69bf20e.js:1:112936 at 1829 (https://divulgacandcontas.tse.jus.br/divulga/829.e77b9db3d69bf20e.js:1:113073) at r (https://divulgacandcontas.tse.jus.br/divulga/runtime.113bd67c35d7d457.js:1:128) at v.invoke (https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js:1:6567) at Object.onInvoke (https://divulgacandcontas.tse.jus.br/divulga/main.764e646b5bc838bc.js:1:409686) at v.invoke (https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js:1:6507) at L.run (https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js:1:1933) at https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js:1:16777 at v.invokeTask (https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js:1:7185) at Object.onInvokeTask (https://divulgacandcontas.tse.jus.br/divulga/main.764e646b5bc838bc.js:1:409502) 1 RootWebArea 'Divulgação de Candidaturas e Contas Eleitorais' 7 navigation 'Dark offcanvas navbar' 9 [i] link 'Divulgação de Candidaturas e Contas Eleitorais' 12 [i] 14 'Eleições Municipais 2024' 15 [i] button 'Toggle navigation' 18 heading 'Eleições Municipais 2024' 19 [i] button 'Close' 23 [i] 25 'Eleições' 27 [i] 29 'Atas de Convenção' 31 [i] 33 'Diplomação' 39 contentinfo 42 'TSE - Tribunal Superior Eleitoral' 45 'Versão 2.8.12' ```

Other candidate routes with the same result:

Expected behavior

The first URL should render the public profile for DR THIAGO PEÇANHA, a 2024 mayoral candidate in Itapemirim, ES.

Firefox renders the profile successfully after a hard reload. Its console has no JavaScript errors; the only unrelated messages are font glyph bounding-box adjustments and a 403 for favicon.ico.

The Firefox HAR shows that, after loading the same lazy chunk, the application successfully requests these resources (all HTTP 200):

/divulga/rest/v1/eleicao/eleicao-atual?idEleicao=2045202024
/divulga/rest/v1/candidatura/buscar/2024/56553/2045202024/candidato/80002363211
/divulga/rest/v1/prestador/consulta/2045202024/2024/56553/11/40/40/80002363211

Actual behavior

Lightpanda loads the document and successfully completes these initial XHRs:

/divulga/rest/v1/sistema
/divulga/rest/v1/ata/ordinarias
/divulga/rest/v1/eleicao/anos-eleitorais

It then fetches and starts executing this lazy-loaded module:

https://divulgacandcontas.tse.jus.br/divulga/829.e77b9db3d69bf20e.js

Execution immediately logs this error:

Error: Uncaught (in promise): TypeError
TypeError
    at https://divulgacandcontas.tse.jus.br/divulga/829.e77b9db3d69bf20e.js:1:112936
    at 1829 (https://divulgacandcontas.tse.jus.br/divulga/829.e77b9db3d69bf20e.js:1:113073)
    at r (https://divulgacandcontas.tse.jus.br/divulga/runtime.113bd67c35d7d457.js:1:128)
    at v.invoke (https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js:1:6567)
    at Object.onInvoke (https://divulgacandcontas.tse.jus.br/divulga/main.764e646b5bc838bc.js:1:409686)
    at v.invoke (https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js:1:6507)
    at L.run (https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js:1:1933)
    at https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js:1:16777
    at v.invokeTask (https://divulgacandcontas.tse.jus.br/divulga/polyfills.260c5b1f06020e59.js:1:7185)
    at Object.onInvokeTask (https://divulgacandcontas.tse.jus.br/divulga/main.764e646b5bc838bc.js:1:409502)

Lightpanda does not make any of the three candidate-specific requests that Firefox makes after loading this chunk, and the rendered semantic tree has no candidate content.

The Firefox HAR + console text for this test is attached: divulgacandcontas.tse.jus.br_2026-08-09.zip

Minimal independent reproduction

This HTML has no dependency on the TSE application or on a network response. Save it as request-open-stream.html and load it in Lightpanda and Firefox.

<!doctype html>
<pre id="result">not run</pre>
<script>
// This is the observable result of the feature probe: Firefox reads `duplex`.
let duplexAccessed = false;

try {
  // This stream is deliberately still open: nothing has closed it yet.
  const stream = new ReadableStream();

  // No fetch() is performed. This only constructs a Request object.
  const request = new Request("https://example.com/", {
    method: "POST",
    body: stream,
    get duplex() {
      duplexAccessed = true;
      return "half";
    },
  });

  document.querySelector("#result").textContent =
    "OK: " + request.method + "; duplex accessed: " + duplexAccessed;
} catch (error) {
  document.querySelector("#result").textContent =
    "ERROR: " + error.name + ": " + error.message;
}
</script>
Browser Result
Firefox OK: POST; duplex accessed: false
Lightpanda nightly 8691568f ERROR: TypeError:

An open stream is important here. A request body stream is allowed to produce data asynchronously; creating a Request should not require that the producer has already closed it. The example also never sends the request, so there is no need to turn the stream into bytes merely to create the object.

Root cause identified from Lightpanda source

This Lightpanda binary identifies commit 8691568f. I cloned lightpanda-io/browser and inspected that exact commit.

The important detail is simple: Lightpanda currently turns a request body into bytes while new Request(...) is running. An open ReadableStream cannot be turned into a complete byte sequence yet, so Lightpanda throws TypeError and never creates the Request.

The sequence in the source is:

  1. Request.init immediately extracts opts.body (around line 125).
  2. For a ReadableStream, body_init.zig calls ReadableStream.collectBodyBytes().
  3. collectBodyBytes() explicitly returns error.TypeError when the stream is still readable (open); it accepts only a closed stream.

In other words, this is not an error from the TSE API, CORS, or an attempted HTTP upload. It happens locally, while JavaScript is constructing a Request object.

The TSE lazy chunk uses this as a Fetch capability probe. Its relevant minified behavior is equivalent to:

let duplexAccessed = false;

new Request(origin, {
  method: "POST",
  body: new ReadableStream(),
  get duplex() {
    duplexAccessed = true;
    return "half";
  },
});

It is not trying to send a streaming upload at that moment. It only checks whether the browser accepts that kind of request. Firefox accepts it; Lightpanda throws; the unhandled exception aborts initialization of the Angular lazy chunk.

There is already a Lightpanda test that documents the current buffering limitation: fetch_readablestream_body_reject expects fetch() with an open stream to reject. That is consistent with the present synchronous buffering implementation, but Request construction uses the same path. Consequently, even a feature probe that does not call fetch() fails.

The needed compatibility behavior is for new Request(url, { method: "POST", body: openReadableStream, duplex: "half" }) to succeed, as it does in Firefox/Chromium. The complete solution may retain or transfer the stream and consume it when the request is sent. If streaming uploads are not yet supported, accepting this constructor case would at least prevent this standard capability check from crashing applications.

Metadata

Metadata

Assignees

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