uwsgi: Send http response and continue execution - python

From the uwsgi documentation:
def application(env, start_response):
start_response('200 OK', [('Content-Type','text/html')])
return [b"Hello World"]
Is it possible to respond to http request(close http connection) and continue execution flow(without any usage of threads/queues/external services etc)?
like this:
def application(env, start_response):
start_response('200 OK', [('Content-Type','text/html')])
end_response(b"Hello World")
#HTTP connection is closed
#continue execution..

TL;DR - if you're using Django, you can skip to the end of the answer.
Yes, there is a way to do this. You can hook on to a .close() method of the object returned by your application callable (or alternatively, wsgi.file_wrapper returned through the env.
Check PEP333, specifically the server side spec part: https://peps.python.org/pep-0333/#the-server-gateway-side:
result = application(environ, start_response)
try:
for data in result:
if data: # don't send headers until body appears
write(data)
if not headers_sent:
write('') # send headers now if body was empty
finally:
if hasattr(result, 'close'):
result.close()
As you can see, result.close() will be called at the very end, by which point the data is already sent.
This is, of course, just some reference code, and it does not deal with upstream and terminating the connection before continuing execution. But well-behaved wsgi servers, such as uwsgi and (presumably) gunicorn, do.
They will signal to the upstream that the request has finished sending by closing the socket (or whatever the upstream protocol requires), and then call .close().
If you are using Django, you are already set, because it has request_finished signal. Here's how it works, it hooks to .close() as described above:
https://github.com/django/django/blob/57c7220280db19dc9dda0910b90cf1ceac50c66f/django/http/response.py#L323

Unfortunately, there is no way to continue the code execution after you have returned the response. It would be much easier if you use multithreading but if not you can workaround it in Flask by adding an AJAX call to your HTML response which will send a POST request to one of the server extra route whose handler function will be the execution code you want after returning the response. Here's one of the possible approach using Flask:
myflaskapp.py
from flask import Flask, render_template_string
import time
app = Flask(__name__)
#app.route('/run', methods=['POST'])
def run():
# this is where you put your "continue execution..." code
# below code is used to test if it runs after HTTP connection close
time.sleep(8)
print('Do something')
return ''
#app.route('/')
def index():
return render_template_string('''
Hello World!
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
$(function() {
$.ajax({
type: "POST",
url: "{{ url_for('run') }}"
});
})
</script>
''')
if __name__ == "__main__":
app.run(host='0.0.0.0')
You can run the server on port 9091 with the command:
uwsgi --http 127.0.0.1:9091 --wsgi-file myflaskapp.py --callable app
To test if it is working or not, you can go to the address localhost:9091. If everything works well, you should see that the page is loaded immediately while the terminal will only print out Do something after 8 seconds have passed, indicating the function run executes after the HTTP connection is closed.

Related

Flask socket IO emit from another module

I have almost read every piece of article available on the internet but nothing seems to work for my case. I have installed flask-socketio and everything works fine until I emit the messages from a module other than app.py.
I have tried several ways to accomplish this and I have also read in the doc about it by using Redis but it also did not work for me. Here are the code snippets that I have.
app.py
from flask import Flask
from flask import request
from flask_socketio import send, SocketIO, emit, join_room
app = Flask(__name__)
# This is to stop force sorting in response, by default jsonify sorts the response keys alphabetically
app.config["JSON_SORT_KEYS"] = False
socketio = SocketIO(app, cors_allowed_origins="*")
#socketio.on('join')
def client_join_room(data):
print(type(data))
room = data['room']
join_room(room)
send('you have entered the room.', room=room)
#app.route('/msg')
def send_message():
socketio.emit("message", "Server message", room='my_room')
return "I got you."
if __name__ == '__main__':
socketio.run(host="0.0.0.0", port=5001, debug=True, app=app)
my_module.py
def some_method():
import app
app.socketio.emit("message", "Some information about process", room='my_room', broadcast=True)
Note that I have imported app inside the method because app.py also imports my_module.py
I am able to join room.
When I call localhost:5001/msg it does emit to 'my_room'.
The emit does not work inside my_module.py and I have no idea why.
I am consoling the messages that I get from the server at the front-end so I know for sure which messages are received and which are not.
Also, the some_method() here is called by an API request from app.py. Just in case if that is relevant.
I have made logger=True and then I get this message printed on the terminal for each emit call. Even with the one inside some_method()
emitting event "message" to my_room [/]
Does that mean message is actually sent? If yes, then why am I not getting it in the jquery at front-end.
This is what I am doing in html page
$(document).ready(function () {
// start up the SocketIO connection to the server
var socket = io.connect('http://localhost:5001/');
// this is a callback that triggers when the "message" event is emitted by the server.
socket.on('message', function(msg){
console.log(msg)
});
socket.emit('join', {room: 'my_room'});
});
Please try and install Redis and eventlet for asynchronous calls and to send messages from other modules. As described in the documentation then you can change your line in app.py to
socketio = SocketIO(app, cors_allowed_origins="*", message_queue='redis://', async_mode='eventlet')

Catching exception when using Response object in Flask does not work

I am trying to catch an exception when using a Response object. My use case is that I want to stream data like described in the official documentation and that there is the possibility of an exception when I get the data that I want to stream:
# test.py
from flask import Flask, Response, jsonify
import werkzeug
app = Flask(__name__)
#app.route("/")
def hello():
def generate():
raise Exception
for i in range(0,10):
yield '1'
try:
return Response(generate())
except Exception:
return jsonify("error") # expected this but instead 500 server error is returned
if __name__ == '__main__':
app.run()
When I then run the server and request the data, I would expect to see "error" but instead an Internal Server Error message from Flask is shown.
FLASK_APP=test.py flask run
curl http://localhost:5000/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to
complete your request. Either the server is overloaded or there is
an error in the application.</p>
In some way the exception gets raised, as we can see it in stdout but does not get catched by Flask.
I also tried to make sure that the exception does not get catched by setting passthrough_errors option of the werkzeug server. Then the exception does not get catched in Werkzeug e.g. here or here. Unfortunately this did not helped for catching it in the Flask application above:
app.run(passthrough_errors=True)
Output of flask --version:
Flask 1.0.2
Python 3.7.0 (default, Aug 22 2018, 15:22:33)
[Clang 9.1.0 (clang-902.0.39.2)]
UPDATE:
This problem also occurs when I am not using the streaming:
from flask import Flask, Response, jsonify
app = Flask(__name__)
#app.route("/")
def hello():
try:
return Response(2)
except Exception as error:
return jsonify({'error': error})
if __name__ == '__main__':
app.run(passthrough_errors=False)
A TypeError gets raised on Flask side, because the Integer is not iterable but the exception does not get catched.
FLASK_APP=test.py flask run
curl http://localhost:5000/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to
complete your request. Either the server is overloaded or there is
an error in the application.</p>
In case you come here from a web search because your exceptions in a generator don't get rendered on the client side: That's because the pattern of generators in responses is really HTTP chunked streaming. This technique results in one HTTP response being sent to the client in chunks. The HTTP response code (e.g. 200 for success) is returned first. So if any of the following chunks trigger an exception in your code, Flask has no possibility of returning that to the browser.
Let's change your code and run it again:
#app.route("/")
def hello():
def generate():
raise Exception
yield '1'
try:
resp = Response(generate())
data = resp.data
return resp
except Exception:
return jsonify("error") # expected this but instead 500 server error is returned
This code will correctly return "error", the previous one also "correctly" raises a 500 server error. To understand why, you have to think of the execution flow. In your initial code, return Response(generate()) will return immediately without executing the generator function. The consumer, in this case probably werkzeug, will try to read data or a similar property which will then cause the generator to run. By that time your function is already executed and of course no exception has happened, since you are returning the generator wrapped in a Response. Hope this helps.
Update
The above code only demonstrates the problem and explains why the exception is not caught in the original code. If you have an error in the middle of your stream IMHO the application should give a 500 server error and die. Moving the exception to the generator however will let you catch such errors:
#app.route("/")
def hello():
def generate():
try:
raise Exception('some error')
for i in range(0,10):
yield '1'
except Exception as e:
yield str(e)
return Response(generate())
And you can't use jsonify because the generator is being consumed outside of your application context.

bottle.py stalls when client disconnects

I have a python server written with bottle. When I access the server from a website using Ajax, and then close the website before the server can send its response, the server gets stuck trying to send the response to a destination that no longer exists. When this happens, the server becomes unresponsive to any requests for about 10 seconds, before resuming normal operations.
How can I prevent this? I would like bottle to immediately stop trying if the website that made the request no longer exists.
I start the server like this:
bottle.run(host='localhost', port=port_to_listen_to, quiet=True)
and the only url exposed by the server is this:
#bottle.route('/', method='POST')
def main_server_input():
request_data = bottle.request.forms['request_data']
request_data = json.loads(request_data)
try:
response_data = process_message_from_scenario(request_data)
except:
error_message = utilities.get_error_message_details()
error_message = "Exception during processing of command:\n%s" % (error_message,)
print(error_message)
response_data = {
'success' : False,
'error_message' : error_message,
}
return(json.dumps(response_data))
Is process_message_from_scenario a long-running function? (Say, 10 seconds?)
If so, your one-and-only server thread will be tied up in that function, and no subsequent requests will be serviced during that time. Have you tried running a concurrent server, like gevent? Try this:
bottle.run(host='localhost', port=port_to_listen_to, quiet=True, server='gevent')

(flask + socket.IO) Result of emit callback is the response of my REST endpoint

Just to give a context here, I'm a node.JS developer, but I'm on a project that I need to work with Python using Flask framework.
The problem is, when a client request to an endpoint of my rest flask app, I need to emit an event using socket.IO, and get some data from the socket server, then this data is the response of the endpoint. But I didn't figured out how to send this, because flask needs a "return" statement saying what is the response, and my callback is in another context.
Sample of what I'm trying to do: (There's some comments explaining)
import socketio
import eventlet
from flask import Flask, request
sio = socketio.Server()
app = Flask(__name__)
#app.route('/test/<param>')
def get(param):
def ack(data):
print (data) #Should be the response
sio.emit('event', param, callback=ack) # Socket server call my ack function
#Without a return statement, the endpoint return 500
if __name__ == '__main__':
app = socketio.Middleware(sio, app)
eventlet.wsgi.server(eventlet.listen(('', 8000)), app)
Maybe, the right question here is: Is this possible?
I'm going to give you one way to implement what you want specifically, but I believe you have an important design flaw in this, as I explain in a comment above. In the way you have this coded, your socketio.Server() object will broadcast to all your clients, so will not be able to get a callback. If you want to emit to one client (hopefully not the same one that sent the HTTP request), then you need to add a room=client_sid argument to the emit. Or, if you are contacting a Socket.IO server, then you need to use a Socket.IO client here, not a server.
In any case, to block your HTTP route until the callback function is invoked, you can use an Event object. Something like this:
from threading import Event
from flask import jsonify
#app.route('/test/<param>')
def get(param):
ev = threading.Event()
result = None
def ack(data):
nonlocal result
nonlocal ev
result = {'data': data}
ev.set() # unblock HTTP route
sio.emit('event', param, room=some_client_sid, callback=ack)
ev.wait() # blocks until ev.set() is called
return jsonify(result)
I had a similar problem using FastAPI + socketIO (async version) and I was stuck at the exact same point. No eventlet so could not try out the monkey patching option.
After a lot of head bangings it turns out that, for some reason, adding asyncio.sleep(.1) just before ev.wait() made everything work smoothly. Without that, emitted event actually never reach the other side (socketio client, in my scenario)

How can I call a specific function/method in a Python script from Javascript(jquery/ajax)

To clarify:
I am running this as cgi via Apache web server. That isn't the problem
My question is regarding a way to specify which function within the Python script to run when I call it via an ajax request.
I read a tutorial that said to pass the function name I want to call as a var. I did that.
In the Python, I tried
Here's my ajax function
$(document).ready(function() {
$.ajax({
url: "monStat.py",
type: "post",
data: {'callFunc':'isRunning'},
success: function(response){
$('#blurg').html(response).fadeIn(1500);
}
});
});
Here's the Python
def main():
if callFunc:
funcResp = isRunning()
else:
print("No function passed")
def isRunning( process_name ):
''' the content '''
You'll need to make your script web-capable. I haven't worked with CGI, so here's an example with Flask:
from flask import Flask, request
app = Flask(__name__)
#app.route('/is_running', methods=['POST'])
def isRunning():
process_name = request.values.get('name', None)
''' the content '''
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000)
Now, you can just send a request to /is_running:
$.ajax({
url: "/is_running",
type: "post",
data: {'name': 'ls'},
success: function(response){
$('#blurg').html(response).fadeIn(1500);
}
});
Even though you don't mention what kind of web framework you are using (if any), I am going to assume from the naming of your url that you are trying to directly call a python script on your server.
The only way for this to work is if your monStat.py script is structured as a CGI script, and hosted on your server accordingly (in a way that CGI scripts will be executed). Your javascript implies that you want to make a POST request to this script, so your python script will need to accept a POST request, and then read the parameters and act on them. You cannot just name a callable as a string in javascript and have the CGI script automatically know what to run. This is the place of a web framework to provide advanced URL handling.
If you are trying to just call a regular python script via a url, that is not going to work. The most basic primitive approach is using the python CGI module. This is good for just learning and getting started, but a very inefficient and dated approach. You should probably look into a web framework: Python WebFrameworks
Update
As you stated you are in fact using a CGI script...
"Routing" is something you get for free when you use web frameworks. It takes the incoming request into the main loop and decides where it should go to be handled. When you use only CGI, you will have to do this yourself. Every time you make a request to the CGI script, it executes just like a normal script, with a normal entrypoint.
In that entrypoint, you have to read the request. If the request is POST and contains "is_running" then you can forward that request off to your is_running() handler. The "function name" is just string data. It is up to your code to read the request and determine what to do.
Here is a super rough example of what it might look like, where you map some acceptable handlers to functions you allow:
#!/usr/bin/env python
import cgi
import cgitb
cgitb.enable()
def isRunning(form):
print "Content-Type: text/html\n"
print "isRunning()"
print form.getvalue('name')
def _error(*args):
print "Content-Type: text/html\n"
print "Error"
HANDLERS = {
'isRunning': isRunning
}
def main():
form = cgi.FieldStorage()
h_name = form.getvalue('callFunc')
handler = HANDLERS.get(h_name, _error)
handler(form)
if __name__ == "__main__":
main()
This is a start:
import cgi
fs = cgi.FieldStorage()
print "Content-type: text/plain\n"
for key in fs.keys():
print "%s = %s" % (key, fs[key].value)

Categories