I’d like to do the following:
raise HttpResponseForbidden()
But I get the error:
exceptions must be old-style classes or derived from BaseException, not HttpResponseForbidden
How should I do this?
Vini.g.fer
11.6k16 gold badges60 silver badges90 bronze badges
asked Jul 8, 2011 at 1:16
if you want to raise an exception you can use:
from django.core.exceptions import PermissionDenied
def your_view(...):
raise PermissionDenied()
It is documented here :
https://docs.djangoproject.com/en/stable/ref/views/#the-403-http-forbidden-view
As opposed to returing HttpResponseForbidden
, raising PermissionDenied
causes the error to be rendered using the 403.html
template, or you can use middleware to show a custom «Forbidden» view.
andyhasit
13.9k7 gold badges47 silver badges50 bronze badges
answered Dec 2, 2011 at 23:32
ChrisChris
12.2k5 gold badges20 silver badges23 bronze badges
8
Return it from the view as you would any other response.
from django.http import HttpResponseForbidden
return HttpResponseForbidden()
answered Jul 8, 2011 at 1:20
5
You can optionally supply a custom template named «403.html» to control the rendering of 403 HTTP errors.
As correctly pointed out by @dave-halter, The 403 template can only be used if you raise PermissionDenied
Below is a sample view used to test custom templates «403.html», «404.html» and «500.html»; please make sure to set DEBUG=False in project’s settings or the framework will show a traceback instead for 404 and 500.
from django.http import HttpResponse
from django.http import Http404
from django.core.exceptions import PermissionDenied
def index(request):
html = """
<!DOCTYPE html>
<html lang="en">
<body>
<ul>
<li><a href="/">home</a></li>
<li><a href="?action=raise403">Raise Error 403</a></li>
<li><a href="?action=raise404">Raise Error 404</a></li>
<li><a href="?action=raise500">Raise Error 500</a></li>
</ul>
</body>
</html>
"""
action = request.GET.get('action', '')
if action == 'raise403':
raise PermissionDenied
elif action == 'raise404':
raise Http404
elif action == 'raise500':
raise Exception('Server error')
return HttpResponse(html)
answered Oct 2, 2014 at 17:11
2
Try this Way , sending message with Error
from django.core.exceptions import PermissionDenied
raise PermissionDenied("You do not have permission to Enter Clients in Other Company, Be Careful")
answered Dec 5, 2018 at 17:29
Saad MirzaSaad Mirza
1,13414 silver badges22 bronze badges
Идея делать нормальный REST на Django – утопия, но некоторые моменты настолько логичные и нет одновременно, что об этом хочется писать. Ниже история про то, как мы сделали ViewSet
от GenericViewSet
и пары миксинов в DRF, покрыли это все тестами и получили местами странные, но абсолютно обоснованные коды ответов.
Текст может быть полезен новичкам (или чуть более прошаренным) в Django, дабы уложить в голове формирование url’ов и порядок вызова методов permission-классов. Ну а бывалые скажут, что все это баловство и надо было использовать GenericApiView
.
Маршрут не определен. 404 или 405?
Стандартная история любого веб приложения — CRUD для пользователя. Решили мы почему-то использовать для этих целей ViewSet
, но ручки нужны были не все и чтобы лишнее не вытаскивать, взяли GenericViewSet
и нужный Mixin
.
Зачем так сложно?
Да, выбор странный, но история умалчивает о причинах такого решения, так что имеем что имеем.
В итоге получили следующую картину:
class UsersViewSet(mixins.UpdateModelMixin, GenericViewSet):
pass
Все, что внутри класса нас пока не интересует, поэтому опустим этот момент.
Также у нас были вот такие пути:
router = SimpleRouter()
router.register("users", UsersViewSet, basename="users")
И захотелось нам проверить, что лишние ручки действительно недоступны (чтобы всякие там мимопроходилы их не трогали) и написать на это все дело тестов.
def test_list_user(auth_free_client_and_user):
client, user = auth_free_client_and_user
response = client.get("/api/users/")
assert response.status_code == 404, response.json()
def test_delete_user(auth_free_client_and_user):
client, user = auth_free_client_and_user
response = client.delete(f"/api/users/{user.id}/")
assert response.status_code == 404, response.json()
Внимание вопрос: будет ли это работать?
Ответ убил
Нет
Человеку, не сильно знакомому с DRF покажется, что наши тесты должны сработать. Но работать они не будут. А чтобы понять почему так происходит, нужно заглянуть в класс Router
из DRF, который и формирует эту ошибку.
Как формируется маршрут
В этой части представлены исходники DRF, которые объясняют почему тесты падают и выдают не те http-статусы, которые ожидались. Если вам интересен конечный результат, можно пролистать сразу до следующего заголовка.
Причина AssertionError в тесте в том, как определены маршруты в классе Router
. Если посмотреть на стандартный SimpleRouter
из DRF увидим следующее (источник листинга):
class SimpleRouter(BaseRouter):
routes = [
# List route.
Route(
url=r'^{prefix}{trailing_slash}$',
mapping={
'get': 'list',
'post': 'create'
},
name='{basename}-list',
detail=False,
initkwargs={'suffix': 'List'}
),
# Dynamically generated list routes. Generated using
# @action(detail=False) decorator on methods of the viewset.
DynamicRoute(
url=r'^{prefix}/{url_path}{trailing_slash}$',
name='{basename}-{url_name}',
detail=False,
initkwargs={}
),
# Detail route.
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
mapping={
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
},
name='{basename}-detail',
detail=True,
initkwargs={'suffix': 'Instance'}
),
# Dynamically generated detail routes. Generated using
# @action(detail=True) decorator on methods of the viewset.
DynamicRoute(
url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
name='{basename}-{url_name}',
detail=True,
initkwargs={}
),
]
Что важно запомнить:
-
определен список из объектов
Route
-
в каждом объекте задаются:
-
url
, который будет сгенерирован -
mapping
— список из http-метода и соответствующего метода нашегоViewSet
-
Еще нам важно увидеть в этом классе следующий метод (источник листинга):
def get_method_map(self, viewset, method_map):
"""
Given a viewset, and a mapping of http methods to actions,
return a new mapping which only includes any mappings that
are actually implemented by the viewset.
"""
bound_methods = {}
for method, action in method_map.items():
if hasattr(viewset, action):
bound_methods[method] = action
return bound_methods
Здесь method_map
это mapping из наших Route
.
Получается, что для:
url=r'^{prefix}{trailing_slash}$' - не вернется ничего, поскольку ни одного метода из mapping нет в нашем ViewSet
url=r'^{prefix}/{lookup}{trailing_slash}$' - вернется словарь {“put”: “update”}
Ну и наконец, если посмотреть на проверку в get_url
все того же SimpleRouter
, то увидим следующее (источник листинга):
# Only actions which actually exist on the viewset will be bound
mapping = self.get_method_map(viewset, route.mapping)
if not mapping:
continue
Итого
Из-за наследования от нашего класса от UpdateModelMixin
SimpleRouter
создал нам маршрут вида /users/:id
, но разрешил там только http-методы PUT и PATCH. Но для DELETE используется тот же маршрут, но другой метод.
Поэтому первый тест на list
будет стучаться на /users
, который мы никак не определяли и будет получать в ответ 404, а вот второй тест на delete
будет стучаться на существующий маршрут с несуществующим методом и получит в ответ 405.
Работающие тесты будут выглядеть вот так:
def test_list_user(auth_free_client_and_user):
client, user = auth_free_client_and_user
response = client.get("/api/users/")
assert response.status_code == 404, response.json()
def test_delete_user(auth_free_client_and_user):
client, user = auth_free_client_and_user
response = client.delete(f"/api/users/{user.id}/")
assert response.status_code == 405, response.json()
Спасибо, Django!
403 или 404. Показываем только “свои” записи.
Казалось бы: ну ладно, не совсем очевидно, но в принципе логично. Запомнили и разошлись. Но на этом история не закончилась и на том же проекте мы снова наткнулись на неожиданные статусы (хоть и вполне объяснимые).
Определим еще один ViewSet
для постов пользователя. Добавим ему permission
-класс, который отвечает за то, можно ли мне как пользователю эти методы вызывать.
class PostViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, UserPermission]
Permission
-класс должен отдать нам 403 код ошибки — доступ запрещен — когда мы попытаемся достать чужой пост.
class UserPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
"""Доступ к объекту."""
if view.action in {"retrieve", "update", "partial_update"}:
return obj.user_id == request.user.id
return False
Но также мы хотим в списке показывать только посты пользователя, поэтому можем переопределить queryset
— запрос, по которому достаются данные и доставать сразу с фильтром по пользователю.
class PostViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, UserPermission]
def get_queryset(self):
"""Фильтруем по пользователю."""
return Post.objects.filter(user=self.request.user)
Теперь у нас во всех методах нашего ViewSet будут сразу данные пользователя и ничего лишнего. Но что произойдет если попытаться изменить чужую статью?
Ожидается, что 403. И если мы хотим покрыть это тестом, то он должен выглядеть как-то так:
def test_update_another_user(auth_client_and_user, another_user):
client, user = auth_client_and_user
response = client.patch(
f"/api/posts/{another_user.id}/",
{
"text": "new amazing text",
},
)
assert response.status_code == 403, response.json()
А как будет на самом деле?
А на самом деле будет вот так:
404
А на самом деле все будет зависеть от того, какой метод определен в нашем permission
-классе.
А метода там два:
-
has_permission
— проверяет возможность действий в принципе; -
has_object_permission
— проверяет возможность действий с конкретным объектом (в нашем случае — постом).
Поскольку мы хотим изменить объект, то нужно определить get_object_permission
. Тогда произойдет следующее: Django сначала выполнит get_queryset
и от него попытается сделать .get()
нашей записи, ничего не найдет и свалится в 404
так и не дойдя до проверки в permission
-классе.
Но, если мы например, не авторизовались, то все-таки получим 403
. Потому что проверка авторизации определена в has_permission
. (Источник листинга)
class IsAuthenticated(BasePermission):
"""
Allows access only to authenticated users.
"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated)
А has_permission
выполняется до того как достается queryset
.
И снова спасибо, Django!
Путь определения статуса
Собираем воедино всё, о чем мы упоминали в тексте.
Порядок выполнения проверок примерно следующий:
-
проверяем существует ли url в принципе — на этом этапе в случае ошибки будет 404;
-
проверяем доступен ли http метод — здесь при неудаче будет 405;
-
выполняем
has_permission
изpermission_classes
— тут 403; -
get_queryset
изViewSet
— тут 404; -
проверяем
has_object_permission
— тут снова 403.
Кстати, еще один забавный нюанс: если вы переопределяете методы retrieve
, update
, delete
в своем ViewSet
, то has_object_permission
может и не вызваться. Подробнее здесь.
Вместо выводов
Как говорится, ежики кололись, плакали, но продолжали жрать кактус пытаться сделать REST на Django.
Каких-то способов это обойти, кроме как не переопределять get_queryset или выкидывать нужные статусы в нужных ручках самостоятельно найдено не было. Надеемся, что кому-то этот текст сохранит пару нервных клеток при попытках понять, почему вместо 404 вы получили 403 или 405.
Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными статьями.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А вы настраивали REST на Django?
52.63%
Да, все прошло нормально
10
15.79%
REST на Django? Зачем?
3
Проголосовали 19 пользователей.
Воздержались 3 пользователя.
-
Getting Help
-
el
-
es
-
fr
-
id
-
it
-
ja
-
ko
-
pl
-
pt-br
-
zh-hans
- Language: en
-
1.8
-
1.10
-
1.11
-
2.0
-
2.1
-
2.2
-
3.0
-
3.1
-
3.2
-
4.0
-
4.1
-
dev
-
Documentation version:
4.2
Built-in Views¶
Several of Django’s built-in views are documented in
Writing views as well as elsewhere in the documentation.
Serving files in development¶
-
static.
serve
(request, path, document_root, show_indexes=False)¶
There may be files other than your project’s static assets that, for
convenience, you’d like to have Django serve for you in local development.
The serve()
view can be used to serve any directory
you give it. (This view is not hardened for production use and should be
used only as a development aid; you should serve these files in production
using a real front-end web server).
The most likely example is user-uploaded content in MEDIA_ROOT
.
django.contrib.staticfiles
is intended for static assets and has no
built-in handling for user-uploaded files, but you can have Django serve your
MEDIA_ROOT
by appending something like this to your URLconf:
from django.conf import settings from django.urls import re_path from django.views.static import serve # ... the rest of your URLconf goes here ... if settings.DEBUG: urlpatterns += [ re_path( r"^media/(?P<path>.*)$", serve, { "document_root": settings.MEDIA_ROOT, }, ), ]
Note, the snippet assumes your MEDIA_URL
has a value of
'media/'
. This will call the serve()
view,
passing in the path from the URLconf and the (required) document_root
parameter.
Since it can become a bit cumbersome to define this URL pattern, Django
ships with a small URL helper function static()
that takes as parameters the prefix such as MEDIA_URL
and a dotted
path to a view, such as 'django.views.static.serve'
. Any other function
parameter will be transparently passed to the view.
Error views¶
Django comes with a few views by default for handling HTTP errors. To override
these with your own custom views, see Customizing error views.
The 404 (page not found) view¶
-
defaults.
page_not_found
(request, exception, template_name=‘404.html’)¶
When you raise Http404
from within a view, Django loads a
special view devoted to handling 404 errors. By default, it’s the view
django.views.defaults.page_not_found()
, which either produces a “Not
Found” message or loads and renders the template 404.html
if you created it
in your root template directory.
The default 404 view will pass two variables to the template: request_path
,
which is the URL that resulted in the error, and exception
, which is a
useful representation of the exception that triggered the view (e.g. containing
any message passed to a specific Http404
instance).
Three things to note about 404 views:
- The 404 view is also called if Django doesn’t find a match after
checking every regular expression in the URLconf. - The 404 view is passed a
RequestContext
and
will have access to variables supplied by your template context
processors (e.g.MEDIA_URL
). - If
DEBUG
is set toTrue
(in your settings module), then
your 404 view will never be used, and your URLconf will be displayed
instead, with some debug information.
The 500 (server error) view¶
-
defaults.
server_error
(request, template_name=‘500.html’)¶
Similarly, Django executes special-case behavior in the case of runtime errors
in view code. If a view results in an exception, Django will, by default, call
the view django.views.defaults.server_error
, which either produces a
“Server Error” message or loads and renders the template 500.html
if you
created it in your root template directory.
The default 500 view passes no variables to the 500.html
template and is
rendered with an empty Context
to lessen the chance of additional errors.
If DEBUG
is set to True
(in your settings module), then
your 500 view will never be used, and the traceback will be displayed
instead, with some debug information.
The 403 (HTTP Forbidden) view¶
-
defaults.
permission_denied
(request, exception, template_name=‘403.html’)¶
In the same vein as the 404 and 500 views, Django has a view to handle 403
Forbidden errors. If a view results in a 403 exception then Django will, by
default, call the view django.views.defaults.permission_denied
.
This view loads and renders the template 403.html
in your root template
directory, or if this file does not exist, instead serves the text
“403 Forbidden”, as per RFC 9110#section-15.5.4 (the HTTP 1.1
Specification). The template context contains exception
, which is the
string representation of the exception that triggered the view.
django.views.defaults.permission_denied
is triggered by a
PermissionDenied
exception. To deny access in a
view you can use code like this:
from django.core.exceptions import PermissionDenied def edit(request, pk): if not request.user.is_staff: raise PermissionDenied # ...
The 400 (bad request) view¶
-
defaults.
bad_request
(request, exception, template_name=‘400.html’)¶
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.
django.views.defaults.bad_request
, is otherwise very similar to the
server_error
view, but returns with the status code 400 indicating that
the error condition was the result of a client operation. By default, nothing
related to the exception that triggered the view is passed to the template
context, as the exception message might contain sensitive information like
filesystem paths.
bad_request
views are also only used when DEBUG
is False
.
Back to Top
status.py
418 I’m a teapot — Any attempt to brew coffee with a teapot should result in the error code «418 I’m a teapot». The resulting entity body MAY be short and stout.
— RFC 2324, Hyper Text Coffee Pot Control Protocol
Using bare status codes in your responses isn’t recommended. REST framework includes a set of named constants that you can use to make your code more obvious and readable.
from rest_framework import status
from rest_framework.response import Response
def empty_view(self):
content = {'please move along': 'nothing to see here'}
return Response(content, status=status.HTTP_404_NOT_FOUND)
The full set of HTTP status codes included in the status
module is listed below.
The module also includes a set of helper functions for testing if a status code is in a given range.
from rest_framework import status
from rest_framework.test import APITestCase
class ExampleTestCase(APITestCase):
def test_url_root(self):
url = reverse('index')
response = self.client.get(url)
self.assertTrue(status.is_success(response.status_code))
For more information on proper usage of HTTP status codes see RFC 2616
and RFC 6585.
Informational — 1xx
This class of status code indicates a provisional response. There are no 1xx status codes used in REST framework by default.
HTTP_100_CONTINUE
HTTP_101_SWITCHING_PROTOCOLS
Successful — 2xx
This class of status code indicates that the client’s request was successfully received, understood, and accepted.
HTTP_200_OK
HTTP_201_CREATED
HTTP_202_ACCEPTED
HTTP_203_NON_AUTHORITATIVE_INFORMATION
HTTP_204_NO_CONTENT
HTTP_205_RESET_CONTENT
HTTP_206_PARTIAL_CONTENT
HTTP_207_MULTI_STATUS
HTTP_208_ALREADY_REPORTED
HTTP_226_IM_USED
Redirection — 3xx
This class of status code indicates that further action needs to be taken by the user agent in order to fulfill the request.
HTTP_300_MULTIPLE_CHOICES
HTTP_301_MOVED_PERMANENTLY
HTTP_302_FOUND
HTTP_303_SEE_OTHER
HTTP_304_NOT_MODIFIED
HTTP_305_USE_PROXY
HTTP_306_RESERVED
HTTP_307_TEMPORARY_REDIRECT
HTTP_308_PERMANENT_REDIRECT
Client Error — 4xx
The 4xx class of status code is intended for cases in which the client seems to have erred. Except when responding to a HEAD request, the server SHOULD include an entity containing an explanation of the error situation, and whether it is a temporary or permanent condition.
HTTP_400_BAD_REQUEST
HTTP_401_UNAUTHORIZED
HTTP_402_PAYMENT_REQUIRED
HTTP_403_FORBIDDEN
HTTP_404_NOT_FOUND
HTTP_405_METHOD_NOT_ALLOWED
HTTP_406_NOT_ACCEPTABLE
HTTP_407_PROXY_AUTHENTICATION_REQUIRED
HTTP_408_REQUEST_TIMEOUT
HTTP_409_CONFLICT
HTTP_410_GONE
HTTP_411_LENGTH_REQUIRED
HTTP_412_PRECONDITION_FAILED
HTTP_413_REQUEST_ENTITY_TOO_LARGE
HTTP_414_REQUEST_URI_TOO_LONG
HTTP_415_UNSUPPORTED_MEDIA_TYPE
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE
HTTP_417_EXPECTATION_FAILED
HTTP_422_UNPROCESSABLE_ENTITY
HTTP_423_LOCKED
HTTP_424_FAILED_DEPENDENCY
HTTP_426_UPGRADE_REQUIRED
HTTP_428_PRECONDITION_REQUIRED
HTTP_429_TOO_MANY_REQUESTS
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE
HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS
Server Error — 5xx
Response status codes beginning with the digit «5» indicate cases in which the server is aware that it has erred or is incapable of performing the request. Except when responding to a HEAD request, the server SHOULD include an entity containing an explanation of the error situation, and whether it is a temporary or permanent condition.
HTTP_500_INTERNAL_SERVER_ERROR
HTTP_501_NOT_IMPLEMENTED
HTTP_502_BAD_GATEWAY
HTTP_503_SERVICE_UNAVAILABLE
HTTP_504_GATEWAY_TIMEOUT
HTTP_505_HTTP_VERSION_NOT_SUPPORTED
HTTP_506_VARIANT_ALSO_NEGOTIATES
HTTP_507_INSUFFICIENT_STORAGE
HTTP_508_LOOP_DETECTED
HTTP_509_BANDWIDTH_LIMIT_EXCEEDED
HTTP_510_NOT_EXTENDED
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
Helper functions
The following helper functions are available for identifying the category of the response code.
is_informational() # 1xx
is_success() # 2xx
is_redirect() # 3xx
is_client_error() # 4xx
is_server_error() # 5xx
I’ve had the same problem on Django 1.2.1 FINAL. Since I knew that Django on our production site would never be updated from 1.0 (for various reasons), I found a workaround which I implemented into my development version of settings.py, leaving the production settings.py untouched.
Create a middleware.py file in your application directory with the following code:
class disableCSRF:
def process_request(self, request):
setattr(request, '_dont_enforce_csrf_checks', True)
return None
Then in your development version of settings.py, insert this into MIDDLEWARE_CLASSES:
'your_app_name.middleware.disableCSRF',
Perhaps not the safest solution, but our Django site is strictly internal, so there is a minimum risk for any type of malicious actions. This solution is simple and doesn’t involve changes to templates/views, and it worked instantly (unlike other I’ve tried).
Hopefully someone in a similar situation to mine will find this useful.
Credit goes to John McCollum, on whose site I’ve found this.