I’m trying to develop a REST provider with OAuth. I’m using Django RESTFramework and DjangoOAuthToolkit. I did a GET and it works perfectly but I’m trying to use a POST and the server responds with {«detail»: «Method ‘POST’ not allowed.»}
This is my code:
# views.py
@api_view(['POST'])
def pruebapost(request):
usuario = User()
access_token = Token.objects.get(
key=request.POST['oauth_token']
)
usuario = access_token.user
content = {'saludo': usuario.username}
return Response(content)
# settings.py
OAUTH_AUTHORIZE_VIEW = 'principal.views.oauth_authorize'
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
),
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.OAuthAuthentication',
),
}
And I’m using this as a «test» client:
import urlparse
import oauth2 as oauth
import requests
consumer_key = "clave"
consumer_secret = "secreto"
consumer = oauth.Consumer(consumer_key, consumer_secret)
client = oauth.Client(consumer)
resource_url = 'http://blablabla.pythonanywhere.com/prueba'
consumer = oauth.Consumer(key='clave', secret='secreto')
token = oauth.Token(key='e7456187a43141af8d2e0d8fa99b95b9',
secret='3wRIKoacff16tcew')
oauth_request = oauth.Request.from_consumer_and_token(
consumer,
token,
http_method='POST',
http_url=resource_url,
parameters={'hola':'pepe'}
)
oauth_request.sign_request(
oauth.SignatureMethod_HMAC_SHA1(),
consumer,
token
)
url = oauth_request.to_url()
response = requests.post(url, oauth_request.to_postdata())
print response.content
I don’t understand what REST Framework documentation says about 405 Method not allowed
«Raised when an incoming request occurs that does not map to a handler method on the view.»
Thanks
asked Apr 10, 2014 at 8:54
4
This was resolved in the comments by user2663554
Problem solved, I miss one slash on the url.
This response code (405) can come from any number of issues, but it generally ends up that either you are using the wrong URL (as in this case), or you are using the wrong request method. Sometimes it’s both!
Quite often I see people getting this issue when they are trying to update an individual resource (/api/res/1
), but they are using the list url (/api/res
) which doesn’t allow the request to be made. This can also happen in the reverse, where someone is trying to create a new instance, but they are sending a POST
request to the individual object.
In some cases, the wrong url is being used, so users are requesting a standard non-API view and thinking it is an API view (/res
instead of /api/res
). So make sure to always check your urls!
answered Dec 20, 2014 at 21:18
Kevin Brown-SilvaKevin Brown-Silva
40.9k41 gold badges203 silver badges237 bronze badges
1
In my case i had a router with same base url
router.register('sales', SalesViewSet, basename='sales')
and my url patterns was
urlpatterns = [
path('', include((router.urls, app_name))),
path('sales/analytics/', Analytics.as_view(), name='create'),
]
I was getting 405 error for sales/analytics/
. The solution was change the order of urlpatterns.
urlpatterns = [
path('sales/analytics/', Analytics.as_view(), name='create'),
path('', include((router.urls, app_name))),
]
answered Feb 15, 2019 at 12:50
NithinNithin
5,49038 silver badges44 bronze badges
class ApiIndexView(APIView)
instead of this please «import from rest_framework import generics» and change it to
class ApiIndexView(generics.ListCreateAPIView)
there are many views in generic listcreateAPIview is used for get and post and createapiview is used only for post methods
answered Dec 10, 2018 at 12:13
My first answer on SO. After trying everything here and being frustrated for the past 6 hours, it finally hit me and I found another reason this might be happening; I, quite simply, was not logged in to /admin. I logged in to /admin and that fixed the «HTTP 405 method not allowed» problem i.e. the post view not coming up. I hope this helps anyone stuck with this kind of issue.
answered Jan 5, 2020 at 18:51
ce0lace0la
1432 silver badges6 bronze badges
Ошибка 405 в Django возникает, когда отправленный HTTP метод не поддерживается на заданный URL-адрес. Эта ошибка возникает из-за того, что вы пытаетесь отправить запрос на недоступный или запрещенный для вашего приложения URL.
Для решения этой проблемы в Django должно быть определено разрешенное HTTP действие. Если вы используете готовый представления Django, убедитесь, что ваш метод совпадает с разрешенным HTTP действием (например, GET, POST, PUT, DELETE и т.д.). Если вы создаете свои представления Django, убедитесь, что вы определили разрешенные HTTP действия.
if request.method == 'POST':
# some code for POST method
elif request.method == 'GET':
# some code for GET method
else:
return HttpResponseNotAllowed(['POST', 'GET'])
Еще одна причина ошибки 405 — это маршрутизация URL-адреса в Django. Убедитесь, что в вашем проекте настроен корректный маршрут (URL-адрес), который соответствует запросу. Если вы используете пользовательские маршруты, убедитесь, что они настроены правильно и соответствуют заданию.
Ошибка 405 Django может также произойти, если CSRF защита не была правильно настроена в вашем проекте. Проверьте вашу настройку CSRF в Django и убедитесь, что вы настроили ее правильно, в противном случае вы можете потерять доступ к своему приложению.
Обработка и отслеживание ошибок и исключений в Django
#3. Маршрутизация, обработка исключений запросов, перенаправления — Django уроки
MultiValueDictKeyError/django/pygame.rualueDictKeyError: ‘l2-SOLVED
Django : How to display a custom error page for HTTP status 405 (method not allowed) in Django when
Fixed: 405 method not allowed in IIS for \
Django : Why Django doesnt have Error Page Handler for 405 — Method Not Allowed?
Django : 405 error when testing an authed django-rest-framework route
Django : 405 error on django ajax POST
Django : Can’t disconnect user with python-social-auth (Error 405)
Django : 405 POST method not allowed
BLGPG-B6A84BF74B59-23-09-22-12
Новые материалы:
- Английский для программистов python
- Обновить pandas python
- Python перезаписать файл
- Массив массивов python
- Numpy получить столбец
- Python смс рассылка
- Python сортировка по длине
- Pandas тренажер python
- Многопоточный бот вк на python
- Python калькулятор чаевых
- Python бесплатный хостинг
- Как написать шашки на python
- Django тестирование моделей
- Python jupiter notebook перенос строки
I am new in Django REST framework. Can someone explain why I get such error, if I make a POST request to ‘/api/index/’
405 Method Not Allowed
{"detail":"Method \"POST\" not allowed."}
My code is following:
# views.py
class ApiIndexView(APIView):
permission_classes = (permissions.AllowAny,)
def post(self, request, format=None):
return Response("ok")
# urls.py
urlpatterns = [
url(r'^api/index/$', views.ApiIndexView.as_view()),
]
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.DjangoModelPermissions',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
)
}
But if I add <pk>
into my pattern, everything works fine:
# views.py
class ApiIndexView(APIView):
permission_classes = (permissions.AllowAny,)
def post(self, request, pk, format=None):
return Response("ok")
# urls.py
urlpatterns = [
url(r'^api/index/(?P<pk>\d+)/$', views.ApiIndexView.as_view()),
]
I am completely confused. Why it’s necessary to use <pk>
and is there a way to avoid the use of this parameter in the URL pattern?
Идея делать нормальный 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 пользователя.
Loading