-2

I'm using content-type:chunked to transfer data in chunks, separated by \r\n, but the data received by the client is not received in chunks, but all together. How can I solve this problem?

I'm currently using content-type:chunked to transfer data in chunks, separated by \r\n. However, when the client receives the data, it seems to be receiving it all at once, rather than in the expected chunks. I'm unsure why this is happening, and would appreciate any guidance.

Here are some additional details about my setup:

The server is sending the data correctly with properly formatted chunk headers. I've checked the implementation of my client to ensure it is compatible with chunked data transfer. There are no apparent issues with my network connection, but there may be firewalls or proxies that could potentially interfere with chunked transfer encoding.

Any ideas on what might be causing this issue or how to resolve it would be greatly appreciated. Thank you in advance!

The code for the client to read chunked data is

fetch('https://xxxxxxxxxxxxx')
    .then((response) => {
      const stream = response.body.getReader();

      return new ReadableStream({
        async pull(controller) {
          try {
            const { done, value } = await stream.read();
            if (done) {
              controller.close();
            } else {
              controller.enqueue(value);
            }
          } catch (error) {
            controller.error(error);
          }
        },
      });
    })
    .then(async (stream) => {
      const reader = stream.getReader();
      while (true) {
        const { done, value } = await reader.read();
        if (done) {
          break;
        }
      }
    })
    .catch((error) => console.error(error));

The example of the data returned by the server is

{"x":1}\r\n{"x":1}\r\n{"x":1}\r\n{"x":1}\r\n
Faye
  • 1
  • 2

2 Answers2

2

You do not produce chunks just by inserting \r\n into the response payload. To make the server use chunked encoding, use res.write. But if the chunks come too quickly, the consuming client might still combine several of them into one stream.read() response.

The following server-side code makes your client receive three chunks of 9 bytes each.

http.createServer(function(req, res) {
  res.write(`{"x":1}\r\n`);
  setTimeout(function() {
    res.write(`{"x":1}\r\n`);
  }, 100);
  setTimeout(function() {
    res.end(`{"x":1}\r\n`);
  }, 200);
});

But reducing the timeout to 0 gives one chunk of 27 bytes on the client, even though the server still uses chunked encoding.

To summarize, your client should not rely on the chunk boundaries and should not, for example, assume that every chunk on its own is well-formed JSON.

Perhaps what you really want is separate the response by newlines (\r\n). The following client-side code does this with two for loops: The outer adds a chunk to the end of a buffer and the inner consumes lines from the beginning of the buffer. The separation into chunks is therefore independent of the separation into lines.

fetch(...).then(async function(response) {
  var stream = response.body.pipeThrough(new TextDecoderStream()).getReader();
  var buffer = "";
  for (; ;) {
    var { value, done } = await stream.read();
    if (done) break;
    buffer += value;
    for (; ;) {
      var offset = buffer.indexOf("\r\n");
      if (offset >= 0) {
        console.log(buffer.substring(0, offset));
        buffer = buffer.slice(offset + 2);
      } else
        break;
    }
  }
});

But as gre_gor's answer shows, such a splitting of the response can more easily be achieved with server-sent events. Note that these are split at double newlines (that is, at empty lines).

Heiko Theißen
  • 12,807
  • 2
  • 7
  • 31
  • Thank you very much for your response. Is using setTimeout the only solution available? Are there any other alternatives? I'm concerned that using setTimeout may potentially increase the response time of the API. – Faye Apr 08 '23 at 09:46
  • As I said, chunks should not be used to convey additional information to the client. But it seems to me that is what you want to achieve. Can you explain your underlying requirement (https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem)? – Heiko Theißen Apr 08 '23 at 09:49
  • Well, my potential need is that I've written an API where the frontend can only display specific page content after receiving data from the API. However, in order to optimize page efficiency, I'm exploring whether there's a way to return the content to the frontend in chunks. – Faye Apr 08 '23 at 10:03
0

Content-Type: chunked is not a thing. You probably mean Transfer-Encoding: chunked, but that requires a preceding line with the chunk's length in hexadecimal.
That encoding is meant to be used when sending large amounts of data without knows size in advance and letting the client have data in chunks.

You could make it work with that, but there is a better way to do what you want. You can use Server-sent events, which has native support in browsers.

Server:

app.get("/sse", async (req, res) => {
    res.writeHead(200, {
        "Content-Type": "text/event-stream",
        "Connection": "keep-alive",
        "Cache-Control": "no-cache"
    });
    for (let i=0; i<10; i++) {
        let data = {
            i,
            time: (new Date()).toString(),
        };
        res.write("data: "+JSON.stringify(data)+"\n\n");
        await sleep(1000);
    }
    res.end();
});

Client:

const events = new EventSource("/sse");
events.onmessage = event => {
    const data = JSON.parse(event.data);
    console.log(data);
};
gre_gor
  • 6,669
  • 9
  • 47
  • 52
  • I've tried using SSE and have successfully read data through the EventStream. However, I still have an issue where I simulated a scenario where I first returned a portion of the data, then delayed the second portion for one second. I expected to receive the two pieces of data separately at the corresponding times, but in reality, I had to wait for one second before receiving both pieces of data. – Faye Apr 09 '23 at 06:53
  • @Faye My server example delays the messages by one second and they get received by the client every second. If you have a problem with your SSE implementation, then ask another question and this time actually show your server code. – gre_gor Apr 09 '23 at 11:49
  • @Faye, the client receives one server-sent event for every empty line that the server produces. Whether the portion between two empty lines is sent in one chunk or several chunks does not affect the number of server-sent events or their content. – Heiko Theißen Apr 11 '23 at 10:59
  • The problem with `EventSource` is POSTing data to an endpoint, which is supported by `fetch`. – ggorlen Jun 08 '23 at 19:26