Search code examples
javascriptaxiosserver-sent-events

How can I use Axios with Server-sent events (SSE) in the browser?


I'm trying to receive Server-sent events (SSE) in the browser. My application uses axios (v1.6.8) to make API calls. This code (courtesy of this answer) works in node:

const getStreamAxios = () => {
  axios.get('https://my-domain.com/api/getStream', {
    responseType: 'stream',
    headers: {
      'Accept': 'text/event-stream',
    }
  })
    .then(response => {
      console.log('axios got a response');
      const stream = response.data;

      stream.on('data', data => {
          console.log(data.toString('utf8'));
      });
      
    })
    .catch(e => {
      console.error('got an error', e);
    });    
}

But in the browser, the Promise is never fulfilled (i.e.: I don't get the console message "axios got a response"). Instead I get this warning in the console:

The provided value 'stream' is not a valid enum value of type XMLHttpRequestResponseType.

The issue is not in the API endpoint; in addition to checking it with node, I have verified it also works with Apidog and EventSource in the browser.


Solution

  • The root of the problem is, axios as of v1.6.8, uses XMLHttpRequest in the browser (see axios docs). This means you can only use responseType: 'stream' in node.

    This was finally addressed in v1.7.0, which allows you to use fetch instead of XHR, like so:

    const getStreamAxios = () => {
      axios.get('https://my-domain.com/api/getStream', {
        headers: {
          'Accept': 'text/event-stream',
        },
        responseType: 'stream',
        adapter: 'fetch', // <- this option can also be set in axios.create()
      })
        .then(async (response) => {
          console.log('axios got a response');
          const stream = response.data;      
    
          // consume response
          const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
          while (true) {
            const { value, done } = await reader.read();
            if (done) break;
            console.log(value);
          }
        })
        // catch/etc.
    }
    

    If you are really invested in an older version of axios, there is a dodgy-looking workaround, using the onDownloadProgress option:

    const getStreamAxios = () => {
      axios.get('https://my-domain.com/api/getStream', {
        headers: {
          'Accept': 'text/event-stream',
        },
        onDownloadProgress: (evt) => {
          // Parse response from evt.event.target.responseText || evt.event.target.response
          // The target holds the accumulator + the current response, so basically everything from the beginning on each response
          // Note that it's evt.target instead of evt.event.target for older axios versions
          let data = evt.event.target.responseText;
          console.log(data);
        }
      })
    

    Caveat

    Note that, regardless of the option you choose, you will still have to write something to consume the server-sent events. Here's an example using @server-sent-stream/web:

    const getStreamAxios = () => {
      // axios.get(...) etc.
      .then(async (response) => {
        console.log('axios got a response');
        const stream = response.data; // <- should be a ReadableStream
    
        const decoder = new EventSourceStream();
        stream.pipeThrough(decoder);
    
        // Read from the EventSourceStream
        const reader = decoder.readable.getReader();
    
        while (true) {
          const { value, done } = await reader.read();
          if (done) break;
    
          // The value will be a `MessageEvent`.
          // MessageEvent {data: 'message data', lastEventId: '', …}
          console.log(value)
        }
      })
    }
    

    If you don't want to parse this yourself, you can look at the native EventSource API. If you need more options not provided by EventSource (such as setting additional headers), you can take a look at options like sse.js, or this extended EventSource implementation