29

Is it possible to raise BadRequest as exception in django?

I have seen that you can raise a 404 [1].

Use case: in a helper method I load a json from request.GET. If the json was cut since the browser (IE) cut the url, I would like to raise a matching exception.

A BadRequest exception looks appropriate, but up to now there seems to no such exception in django.

In 1.6 there is a SuspiciousOperation exception. But this does not match in my case, since it is not security related.

Of course I could put a try..except around my helper method in the view method, but this is not DRY.

Has someone a solution where I don't need a try..exception around every call of my helper method?

[1] https://docs.djangoproject.com/en/1.6/ref/exceptions/#django.core.urlresolvers.Resolver404

Update

Code example:

def my_view(request):
    data=load_data_from_request(request) # I don't want a try..except here: DRY
    process_data(data)
    return django.http.HttpResponse('Thank you')

def load_data_from_request(request):
    try:
        data_raw=json.loads(...)
    except ValueError, exc:
        raise BadRequest(exc)
    ...
    return data
guettli
  • 25,042
  • 81
  • 346
  • 663
  • 3
    why not to just return `django.http.HttpResponseBadRequest` ? – ambi Aug 21 '14 at 09:05
  • If you want to be DRY, let your helper method return the appropriate `HttpResponse` class, or create another shortcut around it, like `load_json_or_bad_request()`. – Germano Aug 21 '14 at 09:09
  • @ambi I don't want to return `django.http.HttpResponseBadRequest` since my method returns the parsed data, except the input data is broken. – guettli Aug 21 '14 at 11:15
  • 1
    @guettli After your update, I think you should raise a custom exception and handle it in a middleware as suggested by coldmind. It's the same mechanism used by `get_object_or_404` – Germano Aug 21 '14 at 11:56

8 Answers8

29

The other answers are explaining how to return an HTTP response with 400 status.

If you want to hook into Django's 400 error handling, you can raise a SuspiciousOperation exception or a subclass of it.

See the docs here and here.

In your example it would look like:

from django.core.exceptions import SuspiciousOperation

def load_data_from_request(request):
    try:
        data_raw = json.loads(...)
    except ValueError:
        raise SuspiciousOperation('Invalid JSON')
    # ...
    return data
djvg
  • 11,722
  • 5
  • 72
  • 103
yprez
  • 14,854
  • 11
  • 55
  • 70
  • 7
    SuspiciousOperation does not seem appropriate. The request is invalid, not in any way that is security-related. – Jonathan Hartley Jan 29 '18 at 16:56
  • 2
    Then you can subclass it and call it something else. I think that "practicality beats purity" is applicable here... – yprez Feb 04 '18 at 13:40
  • 1
    That's an objectively bad idea. Anyone trying to catch SuspiciousOperation for security reasons will also catch your exception. – Jonathan Hartley Feb 04 '18 at 21:15
  • 3
    How do you tell between security reasons and bad input? If there's a field missing it should obviously be a "bad request". But what if the malformed json is someone trying to break your system? (i.e. writing a custom handler seems like overkill to me, but I can be convinced otherwise :)) – yprez Feb 05 '18 at 11:35
  • Fair enough, I can see that your solution requires a *lot* less code than my 'custom handler' solution, which is a big plus, and also has resulted in my answer appearing a lot lower down on this page. :-) There are some apps which have needed the fine-grained flexibility that custom handler provides, but I agree that doesn't apply to most projects. – Jonathan Hartley Feb 06 '18 at 16:14
  • 1
    From the [docs](https://docs.djangoproject.com/en/3.1/ref/views/#the-400-bad-request-view): "When a `SuspiciousOperation` is raised in Django, it may be handled by a component of Django (for example resetting the session data). If not specifically handled, Django will consider the current request a ‘bad request’ instead of a server error." – djvg Aug 11 '20 at 10:22
14

You need custom middleware to handle exception what you raise. Utilize custom exceptions to check for this condition in middleware.

class ErrorHandlingMiddleware(object):
    def process_exception(self, request, exception):
        if not isinstance(exception, errors.ApiException): # here you check if it yours exception
            logger.error('Internal Server Error: %s', request.path,
                exc_info=traceback.format_exc(),
                extra={
                    'request': request
                }
            )
        # if it yours exception, return response with error description
        try:
            return formatters.render_formatted_error(request, exception) # here you return response you need
        except Exception, e:
            return HttpResponseServerError("Error During Error Processing")
Brand0R
  • 1,413
  • 11
  • 17
coldmind
  • 5,167
  • 2
  • 22
  • 22
  • good point, just keep in mind that the comment line is incorrect, since you return response (in the same way) in any case not just in "your exception" case. This could be perfect or wrong depending the case, I am just making a clarification for the comment line – stelios Feb 20 '17 at 22:14
  • Also this works for versions < 1.10 see: https://docs.djangoproject.com/en/1.10/releases/1.10/#new-style-middleware and https://github.com/wagtail/wagtail/issues/2924 and – stelios Feb 21 '17 at 11:17
  • I think the overarching pattern here is great, but I'm really unhappy about the idea that the renderer of an error is the thing that decides what HTTP code gets returned. I have an answer near the bottom of this page that does a similar thing, but using a decorator on the view instead of middleware. Perhaps my favorite answer is using coldmind's middleware above, but with something like my "exception handler to HTTP return value" code inserted into it. – Jonathan Hartley Jan 29 '18 at 17:15
9

As an alternative to @coldmind's answer (converting exceptions in a middleware layer), you could put a decorator on your view function which does the same thing. Personally I prefer this, because it's just plain-old-Python, and doesn't require me dust off my knowledge of how Django middleware works.

You don't want to stream-of-conciousness inline all functionality in your view functions (this makes your view module depend on all your project's other modules, leading to 'everything depends on everything else' architecture) Instead, it's better if the view just knows about http. It extracts what you need from the request, delegates to some other 'business logic' function. The business logic might delegate to other modules (e.g. database code or interfaces to other external systems.) Then finally the return value from your business logic is converted into an http response by the view function.

But how to communicate errors back to the view function from the business logic (or whatever it delegates to)? Using return values is irksome for many reasons. For example, these error return values will have to be propogated back to the view from all through your whole codebase. This is often cripplingly messy because you will already be using the return values of functions for other purposes.

The natural way to deal with this is to use exceptions, but the Django view won't, by itself, convert uncaught exceptions into returned HTTP status codes (except for a couple of special cases, as the OP says.)

So. I write a decorator to apply to my view. The decorator converts various raised exception types into different returned django.http.HttpResponseXXX values. e.g:

# This might be raised by your business logic or database code, if they get
# called with parameters that turn out to be invalid. The database code needs
# know nothing about http to do this. It might be best to define these exception
# types in a module of their own to prevent cycles, because many modules 
# might need to import them.
class InvalidData(Exception):
    pass

# This decorator is defined in the view module, and it knows to convert
# InvalidData exceptions to http status 400. Add whatever other exception types
# and http return values you need. We end with a 'catch-all' conversion of
# Exception into http 500.
def exceptions_to_http_status(view_func):
    @wraps(view_func)
    def inner(*args, **kwargs):
        try:
            return view_func(*args, **kwargs)
        except InvalidData as e:
            return django.http.HttpResponseBadRequest(str(e))   
        except Exception as e:
            return django.http.HttpResponseServerError(str(e))
     return inner

 # Then finally we define our view, using the decorator.

 @exceptions_to_http_status
 def myview(self, request):
     # The view parses what we need out of incoming requests
     data = request.GET['somearg']

     # Here in the middle of your view, delegate to your business logic,
     # which can just raise exceptions if there is an error.
     result = myusecase(data)

     # and finally the view constructs responses
     return HttpResponse(result.summary)

Depending on circumstance, you might find the same decorator could work on many, or all, of your view functions.

Jonathan Hartley
  • 15,462
  • 9
  • 79
  • 80
6

HttpResponseBadRequest is ready to use. It is implemented as:

class HttpResponseBadRequest(HttpResponse):
    status_code = 400

Edited due OP updated question.

You can create your own helper and encapsulate try-catch block into it.

def myJsonDec(str):
    try:
        ...
dani herrera
  • 48,760
  • 8
  • 117
  • 177
  • I updated the question. Please look at the code example. – guettli Aug 21 '14 at 11:13
  • 1
    HttpResponseBadRequest exceptions are great, but out of the box Django doesn't generalize to handle other HTTP status values this way. (Perhaps it would be very difficult for them to do so - mapping exception types to http status codes is probably quite application-specific, unless you wanted to just explicitly create one exception type per http status code, which means your business logic and code it delegates to would be riddled with knowledge about http status codes.) I have an answer somewhere low down on this page which attempts to address this, but it's a bunch of code. – Jonathan Hartley Mar 26 '16 at 17:09
  • 4
    HttpResponseBadRequest is not an exception, it cannot be raised. – OrangeDog May 09 '18 at 16:41
6

Since 3.2 Django provides BadRequest class. So you can now do

try:
    data_raw = json.loads(...)
except ValueError:
    raise BadRequest('Invalid JSON')

The problem with that is that for some reason the message 'Invalid JSON' doesn't appear in the error response, only generic one is shown:

<!doctype html>
<html lang="en">
<head>
  <title>Bad Request (400)</title>
</head>
<body>
  <h1>Bad Request (400)</h1><p></p>
</body>
</html>
guettli
  • 25,042
  • 81
  • 346
  • 663
David Kubecka
  • 127
  • 1
  • 7
0

I'm not sure what you mean by raising BadRequest as an exception.

You can return a response with any status code you like, either by explicitly using the relevant subclass of HttpResponse, or by adding a status parameter to the normal response.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • I added a code example. I hope it is more clear now. – guettli Aug 21 '14 at 11:14
  • instead of raise Http404 raise Http400, the reason is, I don't want to check function return type and then based on that return Reponse – Serjik Oct 01 '17 at 16:05
0

I think one of the simple way is to define your own BadRequestException and raise it in called function.

from django.http import HttpResponseBadRequest, HttpResponse

class BadRequestException(Exception):
    def __init__(self, message='', *args, **kwargs):
        self.message = message

def my_view(request):
    try:
        data = get_data_from_another_func()
    except BadRequestException as e:
        return HttpResponseBadRequest(e.message)
    process_data(data)
    return HttpResponse('Thank you')

def get_data_from_another_func():
    raise BadRequestException(message='something wrong')

def process_data(data):
    pass
k-brahma
  • 41
  • 3
0

The simpler solution I found is to raise the BadRequest exception. Here is a method I use to return Http400 if the required parameter is not passed in the request. Raising the BadRequest exception will make the request return a 400 status code.

from django.core.exceptions import BadRequest
from django.http import HttpResponse
from django.views import View

def get_param_or_return_400(kwargs: dict, key: str):
    if key in kwargs:
        return kwargs[key]
    raise BadRequest(f'The parameter "{key}" must be provided')

class MyView(View):

    def post(self, request, *args, **kwargs):
        query = get_param_or_return_400(kwargs, 'query')
        # At this point we know query is not None
        do_some_operation(query)
        return HttpResponse('OK')
mrj
  • 589
  • 1
  • 7
  • 17