Deno: Unexpecting req.respond body Deno.Reader behaviour.

Created on 10 Nov 2020  路  5Comments  路  Source: denoland/deno

I try to do a Progressive Rendering HTML with http1.1 "Transfer-Encoding": "chunked".

To try that, I create a fake latency of 100ms on each chunk. But the request send all the chunk at the same time after 1200ms.
I have 12 chunks.

But it should send each chunk each 100ms. Not all chunks at the same time.

To illustrate what I expected, I've wrote the "same" code in Node. and make gifs.

Expected => node/ExpressJS version

this

My deno code :

import { serve } from "https://deno.land/[email protected]/http/server.ts";
import type { ServerRequest } from "https://deno.land/[email protected]/http/server.ts";
import { encode } from "https://deno.land/[email protected]/encoding/utf8.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");

for await (const req of s) {
  $progressiveRendering(req);
}

async function $progressiveRendering(req: ServerRequest) {
  const xmlChunks = [
    "<html><body>",
    "<h1>Hell0 world</h1>",
    "<h2>Hello world</h2>",
    "<h3>Hello world</h3>",
    "<h3>Hello world</h3>",
    "<h3>Hello world</h3>",
    "<h3>Hello world</h3>",
    "<h3>Hello world</h3>",
    "<h3>Hello world</h3>",
    "<h3>Hello world</h3>",
    "<h3>Hello world</h3>",
    "</body></html>",
  ];

  const reader: Deno.Reader = {
    async read(p: Uint8Array) {
      if (!xmlChunks.length) {
        return null;
      } else {
        const chunk = encode(xmlChunks.shift());
        //Should sent a chunk each 100ms
        await sleep(100);
        p.set(chunk);
        return chunk.length;
      }
    },
  };

  async function sleep(ms: number) {
    return new Promise((resolve) => {
      const waitT = setTimeout(() => {
        clearTimeout(waitT);
        resolve();
      }, ms);
    });
  }
  req.respond({
    body: reader,
    status: 200,
    headers: new Headers({
      "Cache-Control": "no-cache",
      "Content-Type": "text/html; charset=utf-8",
      Connection: "keep-alive",
      "X-Content-Type-Options": "nosniff",
      "Transfer-Encoding": "chunked",
    }),
  });
}

Result :

this

What am I missing ?
It is an Deno issue, or it's me the issue ? :p

question std

All 5 comments

I would have to look at it in detail, but I suspect when responding the HTTP server is not flushing and waiting to send until the whole body is read. I've got a few other things going on, so I know I would personally be able to look at it for a couple days, though maybe others have ideas.

Is chunked encoding supported in std/http?

Otherwise you can roll your own:

import { encode } from "https://deno.land/[email protected]/encoding/utf8.ts";
import { delay } from "https://deno.land/[email protected]/async/delay.ts";

for await (let c of Deno.listen({ port: 8000 })) {
  (async function () {
    await c.write(encode([
      "HTTP/1.1 200 OK",
      "content-type: text/html",
      "transfer-encoding: chunked",
      "connection: close",
      "",
      chunk("<!DOCTYPE html>\n<h1>Hello, 1!</h1>\n"),
    ].join("\r\n")));
    await delay(2000);
    await c.write(encode(chunk("<h2>Hello, 2!</h2>\n")));
    await delay(2000);
    await c.write(encode(chunk("<h3>Hello, 3!</h3>\n") + chunk("")));
    c.closeWrite();
  })();
}

function chunk(s: string) {
  return `${s.length.toString(16)}\r\n${s}\r\n`;
}

The BufWriter in writeChunkedBody is not flushed for each chunk. But even adding await writer.flush(); in std/http/_io.ts#L185 doesn't help. It seems to me that the changes are not propagated to the underlying Deno.Writer. I'll investigate more later.

Found a fix.

Instead of wrapping the writer with writer = BufWriter.create(w) in writeChunkedBody, it's possible to just use w. Flushing w gives the expected result.

Will create a PR shortly.

Here's a minimal repro:

// chunked.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { encode } from "https://deno.land/[email protected]/encoding/utf8.ts";
import { delay } from "https://deno.land/[email protected]/async/delay.ts";

for await (let req of serve({ port: 8000 })) {
  let i = 0;
  req.respond({
    body: {
      async read(p: Uint8Array) {
        if (i === 3) return null;
        await delay(1000);
        let chunk = encode(`${(++i).toString()}\n`);
        p.set(chunk);
        return chunk.length;
      },
    },
    headers: new Headers({ "transfer-encoding": "chunked" }),
  });
}
C:\> deno run --allow-net chunked.ts

In another terminal:

C:\>curl localhost:8000
1
2
3

(1, 2, 3 will each be printed after 1s delay.)

If the fix is not applied, 1, 2, 3 will be printed at the same time after 3s.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

watilde picture watilde  路  3Comments

justjavac picture justjavac  路  3Comments

xueqingxiao picture xueqingxiao  路  3Comments

benjamingr picture benjamingr  路  3Comments

motss picture motss  路  3Comments