I am have a python client listening to SSE events from a server with node.js API
The flow is I sent an event to the node.js API through call_notification.py
and run seevents.py
in loop using run.sh
(see below)
However I don't see that python client is receiving this SSE event? any guidance on why is that?
call_notification.py
import requests
input_json = {'BATS':'678910','root_version':'12A12'}
url = 'http://company.com/api/root_event_notification?params=%s'%input_json
response = requests.get(url)
print response.text
node.js API
app.get("/api/root_event_notification", (req, res, next) => {console.log(req.query.params)var events = require('events');var eventEmitter = new events.EventEmitter();//Create an event handler:var myEventHandler = function () {console.log('new_root_announced!');res.status(200).json({message: "New root build released!",posts: req.query.params});}
seevents.py (python client listening to SSE events)
import json
import pprint
import sseclientdef with_urllib3(url):"""Get a streaming response for the given event feed using urllib3."""import urllib3http = urllib3.PoolManager()return http.request('GET', url, preload_content=False)def with_requests(url):"""Get a streaming response for the given event feed using requests."""import requestsreturn requests.get(url, stream=True)url = 'http://company.com/api/root_event_notification'
response = with_urllib3(url) # or with_requests(url)
client = sseclient.SSEClient(response)
#print client.events()
for event in client.events():print "inside"pprint.pprint(json.loads(event.data))
run.sh
#!/bin/shwhile [ /usr/bin/true ]
doecho "Running sseevents.py"python sseevents.py 2>&1 | tee -a sseevents.log.txtecho "sleeping for 30 sec"sleep 30
done
OUTPUT:-
Run call_notification.py on Terminalnode.js API OUTPUTnew_root_announced!
{'root_version': 'ABCD', 'BATS': '143'}./run.sh --> DON'T SEE ABOVE EVENT below
Running sseevents.py
sleeping for 30 sec
Running sseevents.py
sleeping for 30 sec
Running sseevents.py
sleeping for 30 sec
Very short answer to you question:
The server code is not sending a SSE message back to the client.
Why? Because you need to follow the SSE format.
According to JASON BUTZ in Server-Sent Events With Node
You should send a Connection
: keep-alive
header to ensure the client keeps the connection open as well. A Cache-Control
header should be sent with the value no-cache
to discourage the data being cached. Finally, the Content-Type
needs to be set to text/event-stream
.
With all of that done a newline (\n) should be sent to the client and then the events can be sent. Events must be sent as strings, but what is in that string doesn’t matter. JSON strings are perfectly fine.
Event data must be sent in the format "data: <DATA TO SEND HERE>\n"
.
It’s important to note that at the end of each line should be a newline character. To signify the end of an event an extra newline character needs to be added as well.
Multiple data lines are perfectly fine.
Long answer to your question:
According to Eric Bidelman in html5rocks.com:
When communicating using SSEs, a server can push data to your app whenever it wants, without the need to make an initial request. In other words, updates can be streamed from server to client as they happen.
But, in order for this to happen, the client has to "start" by asking for it AND prepare to receive a stream of messages (when they happen).
- The "start" is done by calling a SSE API endpoint (in your case, calling the Node.js API code).
- The preparation is done by preparing to handle a stream of asynchronous messages.
SSEs open a single unidirectional channel between server and client.*
* The emphasis is mine
This means that the server has a "direct" channel to the client. It is not intended to be "started" (opened) by some other process/code that is not "the client" code.
Assuming from OP comments...
Expected behavior (verbose)
A client Alice calls the API endpoint with params {name: "Alice"}
, nothing (visible) happens.
...then a client Bob calls the API endpoint with params {name: "Bob"}
, client Alice receives a SSE with payload {name: "Bob", says: "Hi"}
.
...then a client Carol calls the API endpoint with params {name: "Carol"}
, clients Alice AND Bob each one receives a SSE with payload {name: "Carol", says: "Hi"}
.
...and so on. Every time a new client calls the API endpoint with params, every other client who has a channel "open" will receive a SSE with the new "Hi" payload.
...and then client Bob "disconnects" from the server, client Alice, client Carol and all the clients that have a channel "open" will receive a SSE with payload {name: "Bob", says: "Bye"}
.
...and so on. Every time an old client "disconnects" from the server, every other client who has a channel "open" will receive a SSE with the new "Bye" payload.
Abstracted behavior
- Each new client that asks to "open" a channel sending some params or an old client "disconnects" from the server, they cause and event in the server.
- Every time such an event happens in the server, the server sends a SSE message with the params and a message as payload to all the "open" channels.
Note on blocking Each client with an "open" channel will be "stuck" in an infinite waiting loop for events to happen. It is client design responsibility to use "threading" code techniques to avoid blocking.
Code
Your Python client should "ask" to start the single unidirectional channel AND keep waiting UNTIL the channel is closed. Should not end and start all over again with a different channel. It should keep the same channel open.
From the network perspective, it will be like a "long" response that does not end (until the SSE messaging is over). The response just "keeps coming and coming".
Your Python client code does that. I noted it is the exact sample code used from sseclient-py library.
Client code for Python 3.4
To include the parameters you want to send to the server, use some code from the Requests
library docs/#passing-parameters-in-urls.
So, mixing those samples we end up with the following code as your Python 3.4 client:
import json
import pprint
import requests
import sseclient # sseclient-py# change the name for each client
input_json = {'name':'Alice'}
#input_json = {'name':'Bob'}
#input_json = {'name':'Carol'}url = 'http://company.com/api/root_event_notification'
stream_response = requests.get(url, params=input_json, stream=True)client = sseclient.SSEClient(stream_response)# Loop forever (while connection "open")
for event in client.events():print ("got a new event from server")pprint.pprint(event.data)
Client code for Python 2.7
To include the parameters you want to send to the server, encode them in the URL as query parameters using urllib.urlencode()
library.
Make the http request with urllib3.PoolManager().request()
so you will end up with a stream response.
Note that the sseclient
library returns event data as unicode string. To convert back the JSON object to python object (with python strings) use byteify
, a recursive custom function ( thanks to Mark Amery ).
Use the following code as your Python 2.7 client:
import json
import pprint
import urllib
import urllib3
import sseclient # sseclient-py# Function that returns byte strings instead of unicode strings
# Thanks to:
# [Mark Amery](https://stackoverflow.com/users/1709587/mark-amery)
def byteify(input):if isinstance(input, dict):return {byteify(key): byteify(value)for key, value in input.iteritems()}elif isinstance(input, list):return [byteify(element) for element in input]elif isinstance(input, unicode):return input.encode('utf-8')else:return input# change the name for each client
input_json = {'name':'Alice'}
#input_json = {'name':'Bob'}
#input_json = {'name':'Carol'}base_url = 'http://localhost:3000/api/root_event_notification'
url = base_url + '?' + urllib.urlencode(input_json)http = urllib3.PoolManager()
stream_response = http.request('GET', url, preload_content=False)client = sseclient.SSEClient(stream_response)# Loop forever (while connection "open")
for event in client.events():print ("got a new event from server")pprint.pprint(byteify(json.loads(event.data)))
Now, the server code should:
- emit an inside-server 'hello' event so other clients listen to the event
- "open" the channel
- Register to listen for all possible inside-server events to happen (this means, keeping the channel "open" and not sending anything between messages, just keeping the channel "open").
- This includes to emit an inside-server 'goodbye' event so other clients listen to the event WHEN channel is closed by the client/network (and finally "wrap up").
Use the following Node.js API code:
var EventEmitter = require('events').EventEmitter;
var myEmitter = new EventEmitter;function registerEventHandlers(req, res) {// Save received parametersconst myParams = req.query;// Define function that adds "Hi" and send a SSE formated messageconst sayHi = function(params) {params['says'] = "Hi";let payloadString = JSON.stringify(params);res.write(`data: ${payloadString}\n\n`);}// Define function that adds "Bye" and send a SSE formated messageconst sayBye = function(params) {params['says'] = "Bye";let payloadString = JSON.stringify(params);res.write(`data: ${payloadString}\n\n`);}// Register what to do when inside-server 'hello' event happensmyEmitter.on('hello', sayHi);// Register what to do when inside-server 'goodbye' event happensmyEmitter.on('goodbye', sayBye);// Register what to do when this channel closesreq.on('close', () => {// Emit a server 'goodbye' event with "saved" paramsmyEmitter.emit('goodbye', myParams);// Unregister this particular client listener functionsmyEmitter.off('hello', sayHi);myEmitter.off('goodbye', sayBye);console.log("<- close ", req.query);});
}app.get("/api/root_event_notification", (req, res, next) => {console.log("open -> ", req.query);// Emit a inside-server 'hello' event with the received paramsmyEmitter.emit('hello', req.query);// SSE Setupres.writeHead(200, {'Content-Type': 'text/event-stream','Cache-Control': 'no-cache','Connection': 'keep-alive',});res.write('\n');// Register what to do when possible inside-server events happenregisterEventHandlers(req, res);// Code execution ends here but channel stays open// Event handlers will use the open channel when inside-server events happen
})
...continue quoting Eric Bidelman in html5rocks.com:
Sending an event stream from the source is a matter of constructing a plaintext response, served with a text/event-stream Content-Type, that follows the SSE format. In its basic form, the response should contain a "data:" line, followed by your message, followed by two "\n" characters to end the stream
In the client code, the sseclient-py library takes care of interpreting the SSE format so every time the two "\n" characters arrive, the library "iterates" a new "iterable" object (a new event) that has the data
property with the message sent from the server.
This is how I tested the code
- Started server with Node.js API code
- Run a client with only the "Alice" line uncommented (Nothing is seen on this client console yet).
- Run a second client with only "Bob" line uncommented. The console of the first client "Alice" shows: Bob saying "Hi" (Nothing is seen on Bob's client console yet).
- Run a third client with only "Carol" line uncommented. Alice's and Bob's consoles show: Carol saying "Hi" (Nothing is seen on Carol's client console yet).
- Stop/kill Bob's client. Alice's and Carol's consoles show: Bob saying "Bye".
So, code works OK :)