Custom error message json object with flask-restful - flask

It is easy to propagate error messages with flask-restful to the client with the abort() method, such as
abort(500, message="Fatal error: Pizza the Hutt was found dead earlier today
in the back seat of his stretched limo. Evidently, the notorious gangster
became locked in his car and ate himself to death.")
This will generate the following json output
{
"message": "Fatal error: Pizza the Hutt was found dead earlier today
in the back seat of his stretched limo. Evidently, the notorious gangster
became locked in his car and ate himself to death.",
"status": 500
}
Is there a way to customise the json output with additional members? For example:
{
"sub_code": 42,
"action": "redirect:#/Outer/Space"
"message": "You idiots! These are not them! You've captured their stunt doubles!",
"status": 500
}

People tend to overuse abort(), while in fact it is very simple to generate your own errors. You can write a function that generates custom errors easily, here is one that matches your JSON:
def make_error(status_code, sub_code, message, action):
response = jsonify({
'status': status_code,
'sub_code': sub_code,
'message': message,
'action': action
})
response.status_code = status_code
return response
Then instead of calling abort() do this:
#route('/')
def my_view_function():
# ...
if need_to_return_error:
return make_error(500, 42, 'You idiots!...', 'redirect...')
# ...

I don't have 50 reputation to comment on #dappiu, so I just have to write a new answer, but it is really related to "Flask-RESTful managed to provide a cleaner way to handle errors" as very poorly documented here
It is such a bad document that took me a while to figure out how to use it. The key is your custom exception must inherit from flask_restful import HTTPException. Please note that you cannot use Python Exception.
from flask_restful import HTTPException
class UserAlreadyExistsError(HTTPException):
pass
custom_errors = {
'UserAlreadyExistsError': {
'message': "A user with that username already exists.",
'status': 409,
}
}
api = Api(app, errors=custom_errors)
Flask-RESTful team has done a good job to make custom exception handling easy but documentation ruined the effort.

As #Miguel explains, normally you shouldn't use exceptions, just return some error response. However, sometimes you really need an abort mechanism that raises an exception. This may be useful in filter methods, for example. Note that flask.abort accepts a Response object (check this gist):
from flask import abort, make_response, jsonify
json = jsonify(message="Message goes here")
response = make_response(json, 400)
abort(response)

I disagree with #Miguel on the pertinence of abort(). Unless you're using Flask to build something other than an HTTP app (with the request/response paradigm), I believe that you should use as much of the HTTPExceptions as possible (see the werkzeug.exceptions module). It also means using the aborting mechanism (which is just a shortcut to these exceptions). If instead you opt to explicitly build and return your own errors in views, it leads you into a pattern where you need to check values with a series of if/else/return, which are often unnecessary. Remember, your functions are more than likely operating in the context of a request/response pipeline. Instead of having to travel all the way back to the view before making a decision, just abort the request at the failing point and be done with it. The framework perfectly understands and has contingencies for this pattern. And you can still catch the exception in case you need to (perhaps to supplement it with additional messages, or to salvage the request).
So, similar to #Miguel's but maintaining the intended aborting mechanism:
def json_abort(status_code, data=None):
response = jsonify(data or {'error': 'There was an error'})
response.status_code = status_code
abort(response)
# then in app during a request
def check_unique_username(username):
if UserModel.by__username(username):
json_abort(409, {'error': 'The username is taken'})
def fetch_user(user_id):
try:
return UserModel.get(user_id)
except UserModel.NotFound:
json_abort(404, {'error': 'User not found'})

I had to define attribute code to my subclassed HttpException for this custom error handling to work properly:
from werkzeug.exceptions import HTTPException
from flask_restful import Api
from flask import Blueprint
api_bp = Blueprint('api',__name__)
class ResourceAlreadyExists(HTTPException):
code = 400
errors = {
'ResourceAlreadyExists': {
'message': "This resource already exists.",
'status': 409,
},
}
api = Api(api_bp, errors=errors)
and then later, raise the exception
raise ResourceAlreadyExists

It's obviously late, but in the meanwhile Flask-RESTful managed to provide a cleaner way to handle errors, as pointed out by the docs.
Also the issue opened to suggest the improvement can help.

Using Flask-RESTful (0.3.8 or higher)
from flask_restful import Api
customErrors = {
'NotFound': {
'message': "The resource that you are trying to access does not exist",
'status': 404,
'anotherMessage': 'Another message here'
},
'BadRequest': {
'message': "The server was not able to handle this request",
'status': 400,
'anotherMessage': 'Another message here'
}
}
app = Flask(__name__)
api = Api(app, catch_all_404s=True, errors=customErrors)
The trick is to use the exceptions from Werkzeug Docs
So for instance, if you want to handle a 400 request, you should add BadRequest to the customErrors json object.
Or if you want to handle 404 errors, then use NotFound in your json object
and so on

Related

Logs not coming for endpoint being created in sagemaker

I am trying to create an endpoint via sagemaker. The status of the endpoint goes to failed with the message "The primary container for production variant variantName did not pass the ping health check. Please check CloudWatch logs for this endpoint".
But there are no logs created to check.
Blocked on this from quite some time, is anyone aware why this could be happening
You have missed defining the ping() method in your model_handler.py file.
The model_handler.py file must define two methods, like this -
custom_handler = CustomHandler()
# define your own health check for the model over here
def ping():
return "healthy"
def handle(request, context): # context is necessary input otherwise Sagemaker will throw exception
if request is None:
return "SOME DEFAULT OUTPUT"
try:
response = custom_handler.predict_fn(request)
return [response] # Response must be a list otherwise Sagemaker will throw exception
except Exception as e:
logger.error('Prediction failed for request: {}. \n'
.format(request) + 'Error trace :: {} \n'.format(str(e)))
You should look at the reference code in the accepted answer here.

django drf testing social oauth2 with google

I'm trying to test drf-social-oauth2's integration with Google via python manage.py test with the following test class:
class DRFSocialOAuthIntegrationTest(TestCase):
def setUp(self):
self.test_user = UserModel.objects.create_user("test_user", "test#user.com", TEST_USER_PASSWORD)
self.application = Application(
name="Test Application",
redirect_uris="",
user=self.test_user,
client_type=Application.CLIENT_CONFIDENTIAL,
authorization_grant_type=Application.GRANT_PASSWORD, # https://github.com/jazzband/django-oauth-toolkit/blob/master/oauth2_provider/models.py
)
self.application.save()
# Every test needs a client.
self.client = Client()
def tearDown(self):
self.application.delete()
self.test_user.delete()
def test_drf_social_oauth2_integration(self):
'''Following testing steps found at curl -X POST -d "grant_type=convert_token&client_id=<django-oauth-generated-client_id>&client_secret=<django-oauth-generated-client_secret>&backend=google-oauth2&token=<google_token>" http://localhost:8000/auth/convert-token'''
def convert_google_token(self):
'''
Convert Google token to our access token
curl -X POST -d "grant_type=convert_token&client_id=<client_id>&client_secret=<client_secret>&backend=google-oauth2&token=<google_token>" http://localhost:8000/auth/convert-token
'''
return self.client.post(
'/auth/convert-token',
{
'grant_type': 'convert_token',
'client_id': self.application.client_id,
'client_secret': self.application.client_secret,
'backend': 'google-oauth2',
'token': <google_token>
}
)
That seems to work fine, but I have to keep feeding it the google_token by manually navigating to https://developers.google.com/oauthplayground/?code=<my_code>&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid&authuser=0&prompt=consent#:
Once there, I click on the 'Exchange authorization code for tokens' button, get the access token, and paste that access token into the tokenparameter of the request inside convert_google_token(). This doesn't feel very automated. I'm not sure if I should just click the checkbox so that the access token is refreshed automatically and therefore never have to edit it in convert_google_token(). Or maybe I'm supposed to get that access token programmatically. But I believe that would entail getting the authorization code first, which would have to also be programmatically since it is a one-time code, right?
So in order to get that authorization code, I was trying to parse the html response from developers.google.com/oauthplayground/ like so:
def get_google_authorization_code_html():
import requests
return requests.post(
'https://developers.google.com/oauthplayground/',
json={
'code': <my_code>,
'scope': "email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid",
'authuser': "0",
'prompt': "consent"
}
)
response = get_google_authorization_code_html() #;print(f'Google authorization code html returned: {response.content=}')
self.assertEqual(response.status_code, 200)
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.content) ;print('Google authorization code html return: '); print(soup.prettify())
However, the input element that in the browser shows the authorization code is actually blank in the html response, so I can't retrieve it from there. Would this even be the proper way to go about this? I may be missing key knowledge as I'm pretty new to OAuth2, Django, and testing. I've been trying to read up on it and particularly diagrams such as the one found in this Medium article make me think I'm more or less on the right track, but I'm not sure.
You are making a trivial mistake there. Whenever you need the response of a third-party API for testing purposes, you have to mock the response of the API and also/perhaps mock some of your code as well. In this case, you must mock the drf convert-token, otherwise, you will have to keep reading google's token (and this is not ideal).

Wierd Behavior When Using Python requests.put() with Flask

Background
I have a service A accessible with HTTP requests. And I have other services that want to invoke these APIs.
Problem
When I test service A's APIs with POSTMAN, every request works fine. But when I user python's requests library to make these request, there is one PUT method that just won't work. For some reason, the PUT method being called cannot receive the data (HTTP body) at all, though it can receive headers. On the other side, the POST method called in the same manner receives the data perfectly.
I managed to achieve my goal simply by using httplib library instead, but I am still quite baffled by what exactly happened here.
The Crime Scene
Route 1:
#app.route("/private/serviceA", methods = ['POST'])
#app.route("/private/serviceA/", methods = ['POST'])
def A_create():
# request.data contains correct data that can be read with request.get_json()
Route 2:
#app.route("/private/serviceA/<id>", methods = ['PUT'])
#app.route("/private/serviceA/<id>/", methods = ['PUT'])
def A_update(id):
# request.data is empty, though request.headers contains headers I passed in
# This happens when sending the request with Python requests library, but not when sending with httplib library or with POSTMAN
# Also, data comes in fine when all other routes are commented out
# Unless all other routes are commented out, this happens even when the function body has only one line printing request.data
Route 3:
#app.route("/private/serviceA/schema", methods = ['PUT'])
def schema_update_column():
# This one again works perfectly fine
Using POSTMAN:
Using requests library from another service:
#app.route("/public/serviceA/<id>", methods = ['PUT'])
def A_update(id):
content = request.get_json()
headers = {'content-type': 'application/json'}
response = requests.put('%s:%s' % (router_config.HOST, serviceA_instance_id) + '/private/serviceA/' + str(id), data=json.dumps(content), headers = headers)
return Response(response.content, mimetype='application/json', status=response.status_code)
Using httplib library from another service:
#app.route('/public/serviceA/<id>', methods=['PUT'])
def update_course(id):
content= request.get_json()
headers = {'content-type': 'application/json'}
conn = httplib.HTTPConnection('%s:%s' % (router_config.HOST, serviceA_instance_id))
conn.request("PUT", "/private/serviceA/%s/" % id, json.dumps(content), headers)
return str(conn.getresponse().read())
Questions
1. What am I doing wrong for the route 2?
2. For route 2, the handler doesn't seem to be executed when either handler is commented out, which also confuses me. Is there something important about Flask that I'm not aware of?
Code Repo
Just in case some nice ppl are interested enough to look at the messy undocumented code...
https://github.com/fantastic4ever/project1
The serviceA corresponds to course service (course_flask.py), and the service calling it corresponds to router service (router.py).
The version that was still using requests library is 747e69a11ed746c9e8400a8c1e86048322f4ec39.
In your use of the requests library, you are using requests.post, which is sending a POST request. If you use requests.put then you would send a PUT request. That could be the issue.
Request documentation

raise custom error based on different conditions in resource_owner_authenticator in config/initializers/doorkeeper.rb

I want to raise custom error and customise error response based on different conditions.
for authentication, I have a few conditions where a user is not authenticated, for example,
not active user, user profile is incomplete etc etc.
I followed this, #315 , I configured the same but execution stops at line where it raises the custom doorkeeper error.
Doorkeeper.configure do
resource_owner_authenticator do
#user = User.find(session[:user_id]) || redirect_to(user_login_path(return_to: request.fullpath))
raise Doorkeeper::Errors::OwnError unless #user.status == 'active'
end
end
Please help if anybody have any clue.
I had the same issue until I read the NEWS.md file:
- [#749] Allow user to raise authorization error with custom messages.
Under `resource_owner_authenticator` block a user can
`raise Doorkeeper::Errors::DoorkeeperError.new('custom_message')`
For password flow POST /oauth/token:
resource_owner_from_credentials do |routes|
raise Doorkeeper::Errors::DoorkeeperError.new('custom_message')
end
Gives a response like:
{
"error": "custom_message",
"error_description": "The authorization server encountered an unexpected condition which prevented it from fulfilling the request."
}
You probably have found the answer already, in case you haven't:
Add this to the controller where you use doorkeeper_authorize!
def doorkeeper_unauthorized_render_options(error: nil)
response_hash = { status: false, description: error.description, expired: false }
response_hash[:expired] = true if error.description == "The access token expired"
{ json: response_hash }
end
Here respnse_hash would be the error returned.
Actually, if you want to raise a custom error, you can do something like that with i18n, it's better.
# config/initializers/doorkeeper.rb
Doorkeeper.configure do
resource_owner_authenticator do
raise Doorkeeper::Errors::DoorkeeperError.new('foo') if #bar
end
end
# config/locales/en.yml
en:
doorkeeper:
errors:
messages:
foo: Here we are!
You can always override Doorkeeper::TokensController#create to manage the error as long as your error is inherited from Doorkeeper::Errors::DoorkeeperError.
def create
super
rescue ::Doorkeeper::Errors::DoorkeeperError => e
# Do something here.
handle_token_exception(e)
end

Setting HTTP_REFERER header in Django test

I'm working on a Django web application which (amongst other things) needs to handle transaction status info sent using a POST request.
In addition to the HTTP security supported by the payment gateway, my view checks request.META['HTTP_REFERER'] against an entry in settings.py to try to prevent funny business:
if request.META.get('HTTP_REFERER', '') != settings.PAYMENT_URL and not settings.DEBUG:
return HttpResponseForbidden('Incorrect source URL for updating payment status')
Now I'd like to work out how to test this behaviour.
I can generate a failure easily enough; HTTP_REFERER is (predictably) None with a normal page load:
def test_transaction_status_succeeds(self):
response = self.client.post(reverse('transaction_status'), { ... })
self.assertEqual(response.status_code, 403)
How, though, can I fake a successful submission? I've tried setting HTTP_REFERER in extra, e.g. self.client.post(..., extra={'HTTP_REFERER': 'http://foo/bar'}), but this isn't working; the view is apparently still seeing a blank header.
Does the test client even support custom headers? Is there a work-around if not? I'm using Django 1.1, and would prefer not to upgrade just yet if at all possible.
Almost right. It's actually:
def transaction_status_suceeds(self):
response = self.client.post(reverse('transaction_status'), {}, HTTP_REFERER='http://foo/bar')
I'd missed a ** (scatter operator / keyword argument unpacking operator / whatever) when reading the source of test/client.py; extra ends up being a dictionary of extra keyword arguments to the function itself.
You can pass HTTP headers to the constructor of Client:
from django.test import Client
from django.urls import reverse
client = Client(
HTTP_USER_AGENT='Mozilla/5.0',
HTTP_REFERER='http://www.google.com',
)
response1 = client.get(reverse('foo'))
response2 = client.get(reverse('bar'))
This way you don't need to pass headers every time you make a request.