4

I am implementing some server-side events with EventSource and I need to know when the EventSource connection is established and server responded with initial 200 OK, then I can start performing some other requests which eventually result in messages sent through the EventSource from server.

I actually use this polyfill https://github.com/AlexGalays/EventSource which internally uses XMLHttpRequest.

The problem: When the server sends 200 OK + headers, onreadystatechange is not fired (xhr.readyState is still 1). This is a general issue related to any XHR, not only EventSource.

Example PHP server:

<?php
sleep(5); // our actual implementation does some non-trivial startup here
// send 200 OK + headers, but no data
flush();

sleep(5);
echo "bye";

Example - client:

<script>
    xhr = new XMLHttpRequest();
    xhr.onreadystatechange = () => console.log(`readystate = ${xhr.readyState}`);
    xhr.open('GET', 'http://localhost/longpoll.php');
    xhr.send();
</script>

Expected behavior:

  • readystate = 1 (opened)
  • 5 seconds delay
  • readystate = 2 (headers received)
  • 5 seconds delay
  • readystate = 3 (loading)
  • readystate = 4 (done)

Actual behavior:

  • readystate = 1 (opened)
  • 10 seconds delay
  • readystate = 2 (headers received)
  • readystate = 3 (loading)
  • readystate = 4 (done)

Tested in latest Chrome (77) and Firefox (69), both behave the same.

When I observe the connection in Chrome Dev tools Network tab, I actually do see response headers and status code after the first 5 seconds delay (see https://youtu.be/sIgQnbfwxjM). This means that browser really receives headers and updates connection state after first 5 seconds, but JavaScript is not acknowledged.

Can this be somehow worked around? Or is it some security restriction that prevents me from getting updated connection status in this phase?

amik
  • 5,613
  • 3
  • 37
  • 62

1 Answers1

0

By default Chrome and other browsers buffer the entire response (headers and body) of XMLHttpRequest. So there is no simple option to check headers first and then fetch the body using XMLHttpRequest. The only solution is to handle the response as a stream/chunks, not one single response. There was "moz-chunked-arraybuffer" for xhr.responseType in Firefox but it has been removed. Google Chrome implemented at some point xhr.responseType "stream" (as experimental), but it has been dropped in favor of whatwg Streams. Today the simplest fetch API in most modern browsers gives us possibility to fetch response headers without body (if already sent by the server). Tested in Chrome 77:

let response = await fetch('http://localhost:9615/');

if (response.ok) {
  console.log('status code:', response.status);
  let text = await response.text();
  console.log('text', text);
} else {
  console.error('HTTP-Error:', response.status);
}

You may also use directly mentioned whatwg Streams (ReadableStream), like here:

fetch("http://localhost:9615/").then((response) => {
  const reader = response.body.getReader();
  const stream = new ReadableStream({
    start(controller) {
      console.log('status code:', response.status);
      function push() {
        reader.read().then(({ done, value }) => {
          console.log('read', value);
          if (done) {
            console.log('done');
            controller.close();
            return;
          }

          controller.enqueue(value);
          push();
        });
      };

      push();
    }
  });

  return new Response(stream, { headers: { "Content-Type": "text/html" } });
});

As a test server I used below code (NodeJS):

const http = require('http');

http.createServer(function (req, res) {
    setTimeout(() => {
      res.writeHead(200, {'Content-Type': 'text/plain'});
      res.flushHeaders();
      setTimeout(() => {
        res.write("some content");
        res.end();
      }, 3000);
    }, 3000);
}).listen(9615);

In both solutions (Fetch API and ReadableStream) you will get status code: 200 after first 3000ms and the rest of the response after next 3000ms.

Unfortunately Streams are not yet supported widely by browsers but here is a polyfill. See also this.

Beniamin H
  • 2,048
  • 1
  • 11
  • 17