exhibitionist-digital / ultra

Zero-Legacy Deno/React Suspense SSR Framework
https://ultrajs.dev
MIT License
2.99k stars 65 forks source link

Server crashes with error: Closed already requested. Readable stream is unavailable. #286

Open d9k opened 7 months ago

d9k commented 7 months ago

I have await for request in server.get('*', async (context) => {. at server.tsx.

(I need to obtain Supabase session before SSR rendering start to do SSR routing correctly based on authenticated Supabase user permissions).

If I press [F5] key several times (< 7) in browser then server breaks with error Cannot enqueue chunk when underlying stream is not readable. Readable stream is unavailable. How to fix?!

See

d9k commented 7 months ago
[ultra] - ERROR TypeError: Closed already requested.
    at ReadableByteStreamController.close (ext:deno_web/06_streams.js:5618:13)
    at close (https://esm.sh/v135/react-dom@18.2.0/denonext/server.development.js:5493:23)
    at flushCompletedQueues (https://esm.sh/v135/react-om@18.2.0/denonext/server.development.js:10635:15)
    at abort (https://esm.sh/v135/react-dom@18.2.0/denonext/server.development.js:10672:15)
    at Object.cancel (https://esm.sh/v135/react-dom@18.2.0/denonext/server.development.js:10695:21)
    at Module.invokeCallbackFunction (ext:deno_webidl/00_webidl.js:974:16)
    at ReadableByteStreamController.cancelAlgorithm (ext:deno_web/06_streams.js:3520:14)
    at ReadableByteStreamController.[[[CancelSteps]]] (ext:deno_web/06_streams.js:5710:42)
    at readableStreamCancel (ext:deno_web/06_streams.js:1606:64)
    at ext:deno_web/06_streams.js:2728:15
error: Uncaught TypeError: Readable stream is unavailable.
          controller.enqueue(encodeText(bufferedString));
                     ^
    at transformStreamDefaultControllerEnqueue (ext:deno_web/06_streams.js:3956:11)
    at TransformStreamDefaultController.enqueue (ext:deno_web/06_streams.js:6039:5)
    at https://deno.land/x/ultra@v2.3.8/lib/stream.ts:54:22
    at Object.action (ext:deno_web/02_timers.js:151:11)
    at handleTimerMacrotask (ext:deno_web/02_timers.js:65:10)
    at eventLoopTick (ext:core/01_core.js:189:21)
d9k commented 7 months ago

Can easily be reproduced with

await new Promise((resolve) => setTimeout(resolve, 300));

at server.get('*', async (context) => {

and pressing [F5] 5+ times in browser rapidly

d9k commented 7 months ago

Tried to upgrade Deno (1.37.1 -> 1.38.4).

Error stack changed:

[ultra] - INFO   --> GET / 200 306ms
error: Uncaught TypeError: Readable stream is unavailable.
          controller.enqueue(encodeText(bufferedString));
                     ^
    at transformStreamDefaultControllerEnqueue (ext:deno_web/06_streams.js:4006:11)
    at TransformStreamDefaultController.enqueue (ext:deno_web/06_streams.js:6247:5)
    at https://deno.land/x/ultra@v2.3.8/lib/stream.ts:54:22
    at Object.action (ext:deno_web/02_timers.js:150:11)
    at handleTimerMacrotask (ext:deno_web/02_timers.js:64:10)
    at eventLoopTick (ext:core/01_core.js:184:21)
d9k commented 7 months ago

Rarer error text:

[ultra] - ERROR TypeError: Cannot enqueue chunk when underlying stream is not readable.
    at ReadableByteStreamController.enqueue (ext:deno_web/06_streams.js:5857:13)
    at completeWriting (https://esm.sh/v135/react-dom@18.2.0/denonext/server.development.js:5487:25)
    at flushCompletedQueues (https://esm.sh/v135/react-dom@18.2.0/denonext/server.development.js:10628:13)
    at abort (https://esm.sh/v135/react-dom@18.2.0/denonext/server.development.js:10672:15)
    at Object.cancel (https://esm.sh/v135/react-dom@18.2.0/denonext/server.development.js:10695:21)
    at Module.invokeCallbackFunction (ext:deno_webidl/00_webidl.js:952:16)
    at ReadableByteStreamController.cancelAlgorithm (ext:deno_web/06_streams.js:3553:14)
    at ReadableByteStreamController.[[[CancelSteps]]] (ext:deno_web/06_streams.js:5897:42)
    at readableStreamCancel (ext:deno_web/06_streams.js:1644:64)
    at ext:deno_web/06_streams.js:2761:15
error: Uncaught TypeError: Readable stream is unavailable.
          controller.enqueue(encodeText(bufferedString));
                     ^
    at transformStreamDefaultControllerEnqueue (ext:deno_web/06_streams.js:4006:11)
    at TransformStreamDefaultController.enqueue (ext:deno_web/06_streams.js:6247:5)
    at https://deno.land/x/ultra@v2.3.8/lib/stream.ts:54:22
    at Object.action (ext:deno_web/02_timers.js:150:11)
    at handleTimerMacrotask (ext:deno_web/02_timers.js:64:10)
    at eventLoopTick (ext:core/01_core.js:184:21)
d9k commented 7 months ago

I prepared minimal example with error reproduction:

https://github.com/d9k/d9k-problems-examples/tree/main/deno/ultra/286-readable-stream-is-unavailable

Please help

d9k commented 7 months ago

Solved issue with auth code rewrite using useQuery() + <Suspense> + restructure (supabase providers inside html template) in my project.

After that error with multiple requests became

[ultra] - ERROR TypeError: cannot read headers: request closed
    at Object.get headerList (ext:deno_fetch/23_request.js:117:17)
    at Request.request.<computed> (ext:deno_fetch/23_request.js:570:60)
    at Request.get [headers] (ext:deno_fetch/23_request.js:252:46)
    at Request.get headers (ext:deno_fetch/23_request.js:452:16)
    at getCookie (https://deno.land/x/hono@v3.2.7/middleware/cookie/index.ts:11:28)
    at ServerApp (file:///home/d9k/cr/demo/citations-supabase-demo/server.tsx:93:19)
    at renderWithHooks (https://esm.sh/v135/react-dom@18.2.0/denonext/server.development.j
s:9781:24)
    at renderIndeterminateComponent (https://esm.sh/v135/react-dom@18.2.0/denonext/server.
development.js:9838:23)
    at renderElement (https://esm.sh/v135/react-dom@18.2.0/denonext/server.development.js:
9998:15)
    at renderNodeDestructiveImpl (https://esm.sh/v135/react-dom@18.2.0/denonext/server.dev
elopment.js:10103:17)
Trace: TypeError: cannot read headers: request closed
    at Object.get headerList (ext:deno_fetch/23_request.js:117:17)
    at Request.request.<computed> (ext:deno_fetch/23_request.js:570:60)
    at Request.get [headers] (ext:deno_fetch/23_request.js:252:46)
    at Request.get headers (ext:deno_fetch/23_request.js:452:16)
    at getCookie (https://deno.land/x/hono@v3.2.7/middleware/cookie/index.ts:11:28)
    at ServerApp (file:///home/d9k/cr/demo/citations-supabase-demo/server.tsx:93:19)
    at renderWithHooks (https://esm.sh/v135/react-dom@18.2.0/denonext/server.development.j
s:9781:24)
    at renderIndeterminateComponent (https://esm.sh/v135/react-dom@18.2.0/denonext/server.
development.js:9838:23)
    at renderElement (https://esm.sh/v135/react-dom@18.2.0/denonext/server.development.js:
9998:15)
    at renderNodeDestructiveImpl (https://esm.sh/v135/react-dom@18.2.0/denonext/server.dev
elopment.js:10103:17)
    at errorHandler (https://deno.land/x/hono@v3.2.7/hono-base.ts:53:11)
    at https://deno.land/x/hono@v3.2.7/compose.ts:74:35
    at eventLoopTick (ext:core/01_core.js:192:13)
    at async https://deno.land/x/ultra@v2.3.8/lib/middleware/serveStatic.ts:67:7
    at async https://deno.land/x/ultra@v2.3.8/lib/middleware/serveStatic.ts:67:7
    at async https://deno.land/x/hono@v3.2.7/middleware/logger/index.ts:66:5
    at async https://deno.land/x/hono@v3.2.7/hono-base.ts:335:50
    at async Server.#respond (https://deno.land/std@0.176.0/http/server.ts:299:18)

which doesn't lead to server crash.

But the issue seems very disturbing.

TomokiMiyauci commented 7 months ago

This is reproduced by simply adding asynchronous processing to the handler.

import { createServer } from "ultra/server.ts";

const server = await createServer({
  importMapPath: import.meta.resolve("./importMap.json"),
  browserEntrypoint: import.meta.resolve("./client.tsx"),
});

function wait(ms: number): Promise<void> {
  return new Promise<void>((resolve) => {
    setTimeout(() => resolve(), ms);
  });
}

function App() {
  return <html></html>;
}

server.get("*", async () => {
  await wait(200);

  const result = await server.render(<App />);

  return new Response(result, {
    headers: {
      "content-type": "text/html; charset=utf-8",
    },
  });
});

Deno.serve(server.fetch);

So, this would also occur in a throw promise, using useQuery.

There may be a fatal error in server.render.