SSE (Server-Sent Events) Using A POST Request Without EventSource

David Richards
2 min readFeb 10, 2023

--

This article will explain how to receive SSE from your frontend using a HTTP POST request, which is not supported by EventSource. Most articles and documentation will tell you that a GET request is required to receive the events, but this is not true. Most HTTP libraries will hide the lower level steps used to receive a response from a server like axios. We will need a library that allows us to contruct our own response from each network request received, AKA each SSE message.

How will this work?

  1. First we will use fetch to make the POST request to our SSE endpoint. This endpoint will be setup to return multiple messages with a response header ContentType: text/event-stream.
  const response = await fetch('/sse', {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream'
},
body: {
"user_id": 123
}
})

2. Create a reader instance to get each network request as they are received from the server.

// To recieve data as a string we use TextDecoderStream class in pipethrough
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()

// To receive data as byte array we call getReader() directrly
const reader = response.body.getReader();

3. Create a loop to continue receiving messages until the done signal has been triggered. Inside this loop is also where you will update your frontend app with the SSE messages.

while (true) {
const {value, done} = await reader.read();
if (done) break;
console.log('Received', value);
}

Full Code Examples

Full example of receiving SSE messages from a POST request.

const response = await fetch('/sse', {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream'
},
body: {
"user_id": 123
}
})
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
while (true) {
const {value, done} = await reader.read();
if (done) break;
console.log('Received', value);
}

Sample Express server example to complete the loop for setup.

app.post('/completion', (req, res) => {
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Connection', 'keep-alive')
res.flushHeaders() // flush the headers to establish SSE with client

const response = openai.createCompletion({
model: 'text-davinci-003',
prompt: 'hello world',
max_tokens: 100,
temperature: 0,
stream: true
}, { responseType: 'stream' })

response.then(resp => {
resp.data.on('data', data => {
const lines = data.toString().split('\n').filter(line => line.trim() !== '')
for (const line of lines) {
const message = line.replace(/^data: /, '')
if (message === '[DONE]') {
res.write('data: DONE\n\n')
res.end()
return
}
const parsed = JSON.parse(message)
res.write(`data: ${parsed.choices[0].text}\n\n`)
}
})
}).catch(err => {
console.log(err)
})
})

--

--

David Richards

Founder @ parallellabs.app // davidrichards.tech // Principal Software Engineer. 10+ years working in Bay Area tech, most recently TikTok and Salesforce.