Aborting long running request in Django - django

I want to abort a request to an API which takes longer than 5 seconds. I know the client-side code can have a timeout of 5 seconds and that will ensure it doesn't wait any longer than that to get a response, but the Django server would still be processing the request even though the client won't be receiving it at the end.
As far as I know, even if I configure this on nginx or gunicorn, those will still act as clients for the Django server app and the same problem will occur - that nginx will not wait for more than 5 seconds, but Django will continue processing the request.
Is there any way I can somehow abort this request processing in Django (application server code level) itself?
For context on the specific use case:
This API parses a file and we have a size limit of 5 MB on that file. Even then, for some reason, sometimes, the file parsing takes more than 20-30 seconds, which is why we want to abort the request for this API if it exceeds a certain threshold.

You would have to raise a specific exception after a certain time.
In order to make sure you always return the same error you could use a custom error handler.
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
if isinstance(exc, MyTimeoutException):
return Response(
{'detail': 'Your request timed out.'},
status=status.HTTP_408_REQUEST_TIMEOUT)
response = exception_handler(exc, context)
return response
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}

Related

Fix SSLError in django request to external API

So I have a couple sites made with django and never seen this type of error before.
this site presents data from a API, so I hooked the API up with
try:
r = requests.post(url, data=json.dumps(dados), headers=headers, timeout=30)
except Timeout:
raise EmptyResultSet(f'Erro API TIMEOUT')
if r.status_code == requests.codes.ok:
search = r.json()
else:
search = []
So, I hook he request with the API server, and check for a timeout so django sends me an e-mail about that (with the EmptyResultSet because the site can't display properly when no data is received)
then, if the code is ok it gets the data, and if there is an error it sets search = [] which gets data from a cache later in the code
this snippet was working normally, but then my production server started receiving this error:
HTTPSConnectionPool(host='****', port=443): Max retries exceeded with url: /api/Site/Busca (Caused by SSLError(SSLError('No cipher can be selected.')))
host hidden for safety
So, in local machines the site runs just fine and the people behind the API said my server isn't blacklisted this time, so I don't know where to search for some solution. Django version is 3.2.14, requests is 2.28.1 and urllib3 is 1.26.11

How to make all the error responses for API access in Content-Type of application/json instead of Content-Type of text/html in Flask?

I am building a Flask app that support access by human users and through API. The /api path and its subpaths are dedicated for API access. All API access should receive a response in JSON, i.e., with Content-Type of application/json.
I have organized all API endpoints within a blueprint named api. Using the technique illustrated in Implementing API Exceptions, I have defined a custom error class called ApiAccessError and registered the handler for it in the api blueprint, so whenever ApiAccessError is raised in a view function in the api blueprint, the registered handler for ApiAccessError is invoked to generate a JSON response with Content-Type of application/json.
The issue I have with the current design is that whenever an error that is not an ApiAccessError is raised in handling an API request, the response to the request is not in JSON but in HTML, i.e. having Content-Type of text/html. Such an error can occur, for example, in accessing a GET-only API endpoint with the POST method. In this case, the server response is in HTML with status code of 405. I would like the response to be in JSON while keeping the status code of 405. How can set Flask to respond to the 405 error and all other default errors in JSON instead of HTML in handling API requests?
After further examination of the error handling mechanism through Application Errors, Error Handlers, Custom Error Pages, the source code, and #Jonhasacat's suggestion. I think the best way to accomplish what I wanted in the program is to register an error handler at the app level for HTTPException. In my application, this is done as:
def handle_http_exception(e: HTTPException):
if request.path.startswith(API_PATH):
# Start with the response represented by the error
resp = e.get_response()
# jsonify the response
resp.set_data(
json.dumps(
{
'name': e.name,
'err_msg': e.description
}
)
)
resp.content_type = 'application/json'
return resp
else:
return e
app.register_error_handler(HTTPException, handle_http_exception)
This works because, roughly speaking, all errors raised in the process of handling a request that is an instance of Exception is either an HTTPException or an error to be converted to HTTPException if it doesn't have a handler registered for handling it.

File upload to third party API with HTTP 307 Temporary Redirect in Flask

I have a scenario where I have to upload a file from flask app to a third party API. I have wrapped all API requests in Flask to control API usage. For this I have redirected the traffic from main route towards the api wrapper route with http 307 status to preserve the request body and in the API wrapper I have used request to post into third party API endpoint.
The problem is only file < 100KB gets send through the redirection request, having a file larger than 100 KB gets somehow terminated in the sending phase.
Is there any limit in the 307 redirection and payload size?
I tried debugging by watching the network timing stack trace, from there it seems the request is dropped in the sending phase.
Main Blueprint
#main.route('/upload/',methods=['POST','GET'])
def upload():
#for ajax call
if request.method == 'POST'
return redirect(url_for('api.file_push'),code=307)
else:
return render_template('file-upload.html')
API Blueprint
#api.route('/upload/',methods=['POST'])
def file_push():
upload_file = request.files['file']
filename = urllib.parse.quote(upload_file.filename)
toUpload = upload_file.read()
result=requests.post(apiInterfaces.FILE_UPLOAD_INTERFACE+'/'+filename,files{'file':toUpload})
return result
Yes, I can directly send post request to API endpoint from main route but I don't want to, it will destroy my system design and architecture.
I assume you're using Python, and possibly requests so this answer will be based on what I've learned figuring this out (debugging with a colleague). I filed a bug report with psf/requests. There is a related answer here which confirms my suspicions.
It seems that when you initiate a PUT request using requests (and urllib3), the entire request is sent before a response from the server is looked at, but some servers can send a HTTP 307 during this time. One of two things happen:
the server closes the connection by sending the response, even if the client has not finished sending the entire file. In this case, the client might see a closed connection and you won't have a response you can use for redirect (this happens with urllib3>1.26.10 (roughly)) but requests is not handling this situation correctly
the server sends the response and you re-upload the file to the second location (urllib3==1.26.3 has this behavior when using requests). Technically, there is a bug in urllib3 and it should have failed, but silently lets you upload...
However, it seems that if you are expecting a redirect, the most obvious solution might be to send a null byte via PUT first, get a valid response back for the new URL [don't follow redirects], and then use that response to do the PUT of the full file. With requests, it's probably something like
>>> import requests
>>> from io import BytesIO
>>> data = BytesIO(b'\x00')
>>> response = request.put(url, data=data, allow_redirects=False)
>>> request.put(response.headers['Location'], data=fp, allow_redirects=False)
and at that point, you'll be ok (assuming you only expect a single redirect here).

Django - Render HTTP before database connection

I have a serverless database cluster that spins down when it's out of use. Cold starts take a minute or so, during which django just waits and waits to send the http request object back.
Before I code something complicated, I'm looking for recommendations for an existing middleware/signal integration that would allow me to render an interim view while django attempts to get the database connection (for instance, if the database query takes longer than a second to complete, render to this view instead.)
You could create a custom middleware that tests for DB connectivity on every request. Bear in mind that this will attempt to create a new DB connection on every request
from django.db import connection
from django.db.utils import OperationalError
from django.shortcuts import render_to_response
def db_is_up_middleware(get_response):
def middleware(request):
try:
c = connection.cursor()
except OperationalError:
return render_to_response('your_template.html')
else:
return get_response(request)
return middleware
Partial solution:
I reduced the RESTful gateway's query timeout to 3 seconds. At the end of timeout, I return a 504 Error with a nice message that tells the user that the server has gone to sleep but it will be booting back up shortly. Inside the text/html response of the 504 Error, I included a refresh meta tag, so browsers will automatically try to reload the view.
I took all database calls off the public-facing site and put them behind an authentication layer. This means that authenticating will be the step most likely to time out, and users will (I hope) naturally try to reauthenticate after receiving the 504 Error above.
I added an AJAX jquery call in document.ready() to pull a random database record. This query will time out and nothing will happen (as intended). It forces the database server to begin booting up immediately whenever a user hits a public-facing page.

Gunicorn worker timeout

I have a Django application running in Gunicorn behind Nginx. Everything works fine, exect for one strange thing: I have a "download" view and a RESTful json API. When call the download view I use urllib2 to access the json API to get information. And excactly when I try to do this http get request to the json api, the request times out with an error HTTP Error 504: Gateway Time-out.
When I run the code with ./manage.py runserver everything works fine. The http get request to the json api also only takes a few miliseconds, so no danger of running into a timeout.
Here the Situation in Pseudo code:
myproject/views.py: (accessible as: http://myproject.com/download)
1 def download(request, *args, **kwargs):
2 import urllib2
3 opener = urllib2.build_opener()
4 opener.open('http://myproject.com/api/get_project_stats')
The opener.open() call in line four runs into a timeout when running in Gunicorn, when running with ./manage.py runservereverytihng works fine (and the api call only takes a few miliseconds.
Has anyone had the same problem? And more important: How have you solved it?
I had the same issue using Gunicorn, nGinx, Django and Requests
every time I did:
response = requests.get('http://my.url.com/here')
the workers would timeout
I solved the problem by switching from Syncronous (sync) workers to Asynchronous (eventlet) workers.
if you are launching command line add:
-k 'eventlet'
if you are using a config file add:
worker_class = "eventlet"