简体   繁体   中英

Custom parameter validation with JSON response for Flask-RESTful

I am using Flask-RESTful for creating API endpoints, and I specify the URL this way:

api.add_resource(ListVersionsByStaff, '/shots/versions/<int:project_id>/<int:staff_id>')
api.add_resource(ListSeasons, '/seasons/<int:project_id>')

While Flask-RESTful will return an error response if the given argument is not int , it will return a HTML response.

How can I return a custom JSON error response, for example:

except InvalidParameter as err:
            abort(err.status_code, **err.to_dict())

Checking for the value this way also does not work, the parameter is always type String

class SpecificProject(Resource):
    def get(self, project_id):
        print("DEBUG: project_id is [", project_id, "]", file=sys.stderr)
        print("DEBUG: Type is  [", type(project_id), "]", file=sys.stderr)
        if isinstance(project_id, int):
            pass
        else:
            message = "'{}' is not a valid project_id. Hint: this is a number representing primary key.".format(project_id)
            errors = {}
            errors['resource'] = "Project"
            errors['field'] = "project_id"
            errors['code'] = "invalid"
            errors['stack_trace'] = ""
            abort(400, message=message, errors=errors)

output:

DEBUG: project_id is [ 1 ]
DEBUG: Type is  [ <class 'str'> ]

My solution is to extend the Flask-RESTful Api class and implement my custom error handler. The official documentation explains a little bit about extending , but did not go into sufficient details.

Custom Error Message

All errors in my application is in this structure

class InvalidParameter(Exception):
    status_code = 400

    def __init__(self, message, status_code=None, resource=None,
                field=None, code=None, stack_trace=None):
        Exception.__init__(self)
        self.message = message
        if status_code is not None:
            self.status_code = status_code
        self.resource = resource
        self.field = field
        self.code = code
        self.stack_trace = stack_trace

    def to_dict(self):
        rv = {}
        errors = {}
        rv['message'] = self.message
        errors['resource'] = self.resource
        errors['field'] = self.field
        errors['code'] = self.code
        errors['stack_trace'] = self.stack_trace
        rv['errors'] = errors
        return rv

This is what Flask-RESTful return by default

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server.  If you entered the URL manually please check your spelling and try again.</p>

The output should be JSON, in this format

{
    "message": "The requested URL was not found on the server.  If you entered the URL manually please check your spelling and try again. You have requested this URI [/projects/1asd] but did you mean /projects/<int:project_id> or /projects or /project/new ?",
    "errors": {
        "resource": null,
        "field": null,
        "code": "invalid_url",
        "stack_trace": null
    }
}

Solution

Extend the Api class and overwrite the handle_error method for 404 errors

class CustomApi(Api):
    def handle_error(self, e):
        code = getattr(e, 'code', 404)
        if code == 404:
            response_dict = Api.handle_error(self, e).__dict__
            resp_message = response_dict['response'][0]
            error_message = re.sub(r'.*"(.*)".*', r'\1', resp_message.decode('utf-8'), flags=re.DOTALL)
            err = InvalidParameter(error_message, stack_trace=None,
                                    resource=None, code="invalid_url", field=None)
            return self.make_response(err.to_dict(), 404) #{'message': "something", 'error': {'sub1': 'val1'}}, 404)

        return super(Api, self).handle_error(e)

The handle_error dictionary is in this format

{'headers': Headers([('Content-Type', 'application/json'), ('Content-Length', '263')]), '_status_code': 404, '_status': '404 NOT FOUND', 'direct_passthrough': False, '_on_close': [], 'response': [b'{\n    "message": "The requested URL was not found on the server.  If you entered the URL manually please check your spelling and try again. You have requested this URI [/projects/1asd] but did you mean /projects/<int:project_id> or /projects or /project/new ?"\n}\n']}

I want to re-use the 'response':'message' that was generated, but not in the default format. message was not not properly JSON formatted, so I strip out everything except for the content of message , using this regex

error_message = re.sub(r'.*"(.*)".*', r'\1', resp_message.decode('utf-8'), flags=re.DOTALL)

Note that re.DOTALL is needed to strip out the \\n that was added by Flask-RESTful.

The code that actually builds the JSON response is self.make_response(err.to_dict(), 404)

For all other non-404 errors (eg 400, 500, 503), the error is just passed along to the original Flask-RESTful Api class.

Note that when you create your Flask application, you need to use your custom Api class, and catch all 404 errors:

app = Flask(__name__) 
api = CustomApi(app, catch_all_404s=True)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM