Why is the python client not receiving SSE events?

2024/9/20 15:43:12

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
Answer

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)

  1. A client Alice calls the API endpoint with params {name: "Alice"}, nothing (visible) happens.

  2. ...then a client Bob calls the API endpoint with params {name: "Bob"}, client Alice receives a SSE with payload {name: "Bob", says: "Hi"}.

  3. ...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"}.

  4. ...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.

  5. ...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"}.

  6. ...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:

  1. emit an inside-server 'hello' event so other clients listen to the event
  2. "open" the channel
  3. 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

  1. Started server with Node.js API code
  2. Run a client with only the "Alice" line uncommented (Nothing is seen on this client console yet).
  3. 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).
  4. 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).
  5. Stop/kill Bob's client. Alice's and Carol's consoles show: Bob saying "Bye".

So, code works OK :)

https://en.xdnf.cn/q/72485.html

Related Q&A

sklearn Pipeline: argument of type ColumnTransformer is not iterable

I am attempting to use a pipeline to feed an ensemble voting classifier as I want the ensemble learner to use models that train on different feature sets. For this purpose, I followed the tutorial avai…

PyQT Window: I want to remember the location it was closed at

I have a QDialog, and when the user closes the QDialog, and reopens it later, I want to remember the location and open the window at the exact same spot. How would I exactly remember that location?

Django Reusable Application Configuration

I have some Django middleware code that connects to a database. I want to turn the middleware into a reusable application ("app") so I can package it for distribution into many other project…

executable made with py2exe doesnt run on windows xp 32bit

I created an executable with py2exe on a 64bit windows 7 machine, and distributed the program.On a windows xp 32bit machine the program refuses to run exhibiting the following behavior:a popup window s…

Pandas reading NULL as a NaN float instead of str [duplicate]

This question already has answers here:How to treat NULL as a normal string with pandas?(4 answers)Closed 5 years ago.Given the file:$ cat test.csv a,b,c,NULL,d e,f,g,h,i j,k,l,m,nWhere the 3rd colum…

How to invert differencing in a Python statsmodels ARIMA forecast?

Im trying to wrap my head around ARIMA forecasting using Python and Statsmodels. Specifically, for the ARIMA algorithm to work, the data needs to be made stationary via differencing (or similar method)…

how to see the content of a particular file in .tar.gz archive without unzipping the contents?

for ex abc.tar.gz has abc/file1.txt abc/file2.txt abc/abc1/file3.txt abc/abc2/file4.txt i need to read/display the contents of file3.txt without extracting the file.Thanks for any input.

Matplotlib animation not showing

When I try this on my computer at home, it works, but not on my computer at work. Heres the codeimport numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation import sys im…

Extracting Fields Names of an HTML form - Python

Assume that there is a link "http://www.someHTMLPageWithTwoForms.com" which is basically a HTML page having two forms (say Form 1 and Form 2). I have a code like this ...import httplib2 from …

Best way to combine a permutation of conditional statements

So, I have a series of actions to perform, based on 4 conditional variables - lets say x,y,z & t. Each of these variables have a possible True or False value. So, that is a total of 16 possible per…