Django: 抛出BadRequest异常?

29

在django中是否可能将BadRequest作为异常抛出?

我看到可以抛出404 [1]。

用例:在帮助方法中,我从request.GET加载json。如果json因浏览器(IE)截断了url而被切割,我想引发匹配的异常。

一个BadRequest异常看起来很合适,但到目前为止,在django中似乎没有这样的异常。

在1.6中有一个SuspiciousOperation异常。但是在我的情况下,它与安全无关。

当然,我可以在视图方法中对我的帮助方法进行try..except处理,但这不是DRY的。

是否有人有解决方案,我不需要在每次调用帮助方法时都使用try..exception?

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

更新

代码示例:

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

3
为什么不直接返回 django.http.HttpResponseBadRequest - ambi
如果你想要DRY,让你的辅助方法返回适当的HttpResponse类,或者创建另一个快捷方式,比如load_json_or_bad_request() - Germano
@ambi 我不想返回 django.http.HttpResponseBadRequest,因为我的方法返回解析后的数据,除非输入数据有误。 - guettli
1
@guettli 在你的更新之后,我认为你应该抛出一个自定义异常,并像 coldmind 建议的那样在中间件中处理它。这是 get_object_or_404 使用的相同机制。 - Germano
8个回答

29
其他答案正在解释如何返回带有400状态的HTTP响应。
如果您想要挂钩Django的400错误处理, 您可以引发SuspiciousOperation异常或其子类。
请参阅文档 这里这里
在您的示例中,它应该是这样的:
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

7
SuspiciousOperation 似乎不太适合。该请求无效,与安全性无关。 - Jonathan Hartley
2
然后你可以创建一个子类并给它起一个别的名字。我认为“实用性优于纯粹性”在这里是适用的... - yprez
2
那是一个客观上不好的想法。任何为了安全原因而尝试捕获SuspiciousOperation的人也将捕获您的异常。 - Jonathan Hartley
3
如何区分安全原因和恶意输入?如果某个字段缺失,那显然是“错误的请求”。但如果格式不正确的JSON是有人试图破坏你的系统呢?(例如:编写自定义处理程序似乎对我来说有点过头了,但我可以被说服。) - yprez
很好,我可以看到你的解决方案需要比我的“自定义处理程序”解决方案少得多的代码,这是一个很大的优点,并且导致我的答案在这个页面上出现得更低。 :-) 有一些应用程序需要自定义处理程序提供的细粒度灵活性,但我同意这并不适用于大多数项目。 - Jonathan Hartley
1
文档中得知:"当 Django 中引发了SuspiciousOperation异常时,它可能会被 Django 的某个组件(例如重置会话数据)处理。如果没有特别处理,Django 将把当前请求视为“错误请求”而不是服务器错误。" - djvg

14

你需要自定义中间件来处理你引发的异常。 在中间件中使用自定义异常来检查这种情况。

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")

好的观点,只需记住注释行不正确,因为您在任何情况下返回响应(以相同方式),而不仅仅是在“your exception”情况下。这可能是完美的或错误的,具体取决于情况,我只是为注释行澄清一下。 - stelios
此外,这适用于版本<1.10,请参见:https://docs.djangoproject.com/en/1.10/releases/1.10/#new-style-middleware和https://github.com/wagtail/wagtail/issues/2924。 - stelios
我认为这里的总体模式很好,但我对错误渲染器决定返回HTTP代码的想法非常不满。在本页面底部附近,有一个类似的回答,但是使用视图上的装饰器而不是中间件。也许最喜欢的答案是使用coldmind的中间件,但插入像我“异常处理程序到HTTP返回值”的代码之类的东西。 - Jonathan Hartley

9
作为@coldmind答案的替代方案(在中间件层转换异常),您可以在视图函数上放置一个装饰器来完成同样的事情。个人而言,我更喜欢这种方法,因为它只是普通的Python,不需要我去复习Django中间件的工作方式。
您不希望将所有功能都内联到视图函数中(这会使您的视图模块依赖于项目中的所有其他模块,导致“一切都依赖于彼此”的架构)。相反,最好让视图只知道HTTP。它从请求中提取所需内容,委托给某些其他“业务逻辑”函数。业务逻辑可能会委托给其他模块(例如数据库代码或其他外部系统的接口)。然后,来自您的业务逻辑的返回值由视图函数转换为HTTP响应。
但是如何将错误从业务逻辑(或其委托对象)传递回视图函数?使用返回值有很多缺点。例如,这些错误返回值必须从整个代码库中返回到视图。这通常非常混乱,因为您已经在使用函数的返回值进行其他用途。
处理这种情况的自然方法是使用异常,但Django视图本身不会将未捕获的异常转换为返回的HTTP状态码(除了一些特殊情况,如OP所说)。
因此,我编写了一个装饰器来应用于我的视图。该装饰器将各种引发的异常类型转换为不同的django.http.HttpResponseXXX返回值。例如:
# 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)

根据情况,您可能会发现相同的装饰器可以适用于许多或所有视图函数。


6

自从Django 3.2版本推出了BadRequest类,所以现在你可以这样做:

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

问题在于,由于某些原因,“无效的JSON”消息不会出现在错误响应中,只会显示通用的错误信息:
<!doctype html>
<html lang="en">
<head>
  <title>Bad Request (400)</title>
</head>
<body>
  <h1>Bad Request (400)</h1><p></p>
</body>
</html>

你好,你解决了这个问题吗?我想要自定义一个400错误消息,但是在Pythonic/Django风格的DRY方式下做起来有些困难。 - James M
很遗憾,我没有。 - David Kubecka

6

HttpResponseBadRequest 已经可以使用。 它已经被实现 为:

class HttpResponseBadRequest(HttpResponse):
    status_code = 400

由于问题更新,已进行编辑。

您可以创建自己的辅助程序,并将try-catch块封装到其中。

def myJsonDec(str):
    try:
        ...

我更新了问题,请查看代码示例。 - guettli
1
HttpResponseBadRequest 异常很好用,但 Django 没有通用的方法处理其他 HTTP 状态值。(也许对他们来说这很困难——将异常类型映射到 http 状态码可能非常应用程序特定,除非您只想显式地为每个 http 状态码创建一个异常类型,这意味着您的业务逻辑和委托给它的代码将充斥着关于 http 状态码的知识。)我在这个页面的底部有一个尝试解决此问题的答案,但这是一堆代码。 - Jonathan Hartley
4
HttpResponseBadRequest不是一个异常,它不能被引发。 - OrangeDog

0
我找到的更简单的解决方案是引发 BadRequest 异常。以下是我使用的方法,如果请求中未传递所需参数,则返回 Http400。引发 BadRequest 异常将使请求返回 400 状态码。
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')

0

我不确定你所说的将BadRequest作为异常抛出是什么意思。

你可以返回任何状态码的响应,可以通过显式地使用HttpResponse的相关子类,或者通过向普通响应添加status参数来实现。


我添加了一个代码示例。希望现在更清楚了。 - guettli
不要抛出 Http404,而是抛出 Http400。原因是我不想检查函数返回类型,然后根据返回类型返回响应。 - Serjik

0

我认为其中一种简单的方法是定义自己的BadRequestException并在被调用的函数中引发它。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接