Skip to content

Status code 403 and text from 401 error #9692

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
TooMuchInLove opened this issue Apr 30, 2025 · 5 comments
Open

Status code 403 and text from 401 error #9692

TooMuchInLove opened this issue Apr 30, 2025 · 5 comments

Comments

@TooMuchInLove
Copy link

https://github.com/encode/django-rest-framework/blob/78e97074e7c823ef9c693e4b63ac1e5c6e41ff81/rest_framework/views.py#L491C1-L518C29

Hey guys!
Such a situation has arisen. When I tried to execute a request in my API (being unauthorized), I received a corresponding message stating that I could not do this without authorization.
But I noticed that the text of the message in the response is from error 401, and the error code is from 403.
DRF version djangorestframework >= 3.14, < 3.15.

Problem description:

The whole point is that BasicAuthentication has an implementation of the authenticate_header method (this method is not implemented in the SessionAuthentication class, it simply inherits from BaseAuthentication'). And at the key moment of the program execution, the APIView class (ViewSetinherits fromAPIView) and the get_authenticate_headermethod receive a tuple of theBasicAuthenticationandSessionAuthenticationelements/classes passed toDEFAULT_AUTHENTICATION_CLASSES (settings.py).
Next, the first class from this tuple is strictly taken, and in turn the method authenticate_header is used (in the case of the BasicAuthentication class, the specific value is returned, and the SessionAuthentication class returns `None').

Example (Response):

Code	Details
403      Error: Unauthorized
Undocumented
{
  "type": "client_error",
  "errors": [
    {
      "code": "not_authenticated",
      "detail": "Authentication credentials were not provided.",
      "attr": null
    }
  ]
}

Is this a mistake or was it originally intended that way (and can it be fixed at the library level)?

Here's how I managed to solve my problem, but it looks like a crutch:

class MyViewSet(ViewSet):
    def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)
            print('# dispatch `initial`')

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)
            print(f'# dispatch `initial`: response: {response}')

        except Exception as exc:
            response = self.handle_exception(exc)

            # BEGIN
            # This code --->
            if response.status_code == status.HTTP_403_FORBIDDEN:
                if response.data:
                    errors = response.data['errors']
                    if errors:
                        if errors[0]['detail'] == drf_exceptions.NotAuthenticated.default_detail:
                            response.status_code = status.HTTP_401_UNAUTHORIZED
            # END <---

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response
@TooMuchInLove
Copy link
Author

Link to error classes
https://github.com/encode/django-rest-framework/blob/master/rest_framework/exceptions.py#L176C1-L186C1

@TooMuchInLove
Copy link
Author

Hello everyone :) Can you tell me, please, can anyone help?

@sanyamk23
Copy link

Hey @TooMuchInLove ! 👋

Thanks for the awesome work on DRF — I’ve run into something that might be worth discussing.

🧩 The Issue
While testing unauthenticated access in my API using DRF ≥ 3.14, < 3.15, I noticed something odd:
When a request is made without any authentication credentials, the error message in the response correctly says something like:

"Authentication credentials were not provided."

However, the status code returned is 403 Forbidden, rather than the expected 401 Unauthorized.

Here’s an example response I’m seeing:

{
"type": "client_error",
"errors": [
{
"code": "not_authenticated",
"detail": "Authentication credentials were not provided.",
"attr": null
}
]
}

With this response:
detail: matches the default NotAuthenticated message (✅)
status_code: 403 (❌ should be 401 for an unauthenticated request)

🔍 What's Happening?
After a bit of digging, it looks like this behavior stems from the logic in APIView.get_authenticate_header():

www_authenticate_realm = authenticate_header(self.request)
if www_authenticate_realm:
response.status_code = status.HTTP_401_UNAUTHORIZED
else:
response.status_code = status.HTTP_403_FORBIDDEN

So essentially:
If at least one of the classes in DEFAULT_AUTHENTICATION_CLASSES defines authenticate_header(), DRF will set the response status to 401.
But if none do (like when using only SessionAuthentication), DRF defaults to 403.

This creates a confusing mismatch: the client gets a 403, but the error message is clearly about missing authentication (which by REST/API standards, typically maps to 401).

💡 Workaround I Used
I patched it like this in my ViewSet:

if response.status_code == status.HTTP_403_FORBIDDEN:
if response.data:
errors = response.data.get('errors')
if errors and errors[0]['detail'] == drf_exceptions.NotAuthenticated.default_detail:
response.status_code = status.HTTP_401_UNAUTHORIZED

Yes, I know — not the most elegant solution 😅. But it helps clarify the response for clients.

🛠️ Suggestion
Would it make sense for DRF to always return 401 when the raised exception is NotAuthenticated, regardless of the presence of authenticate_header()? This would align better with expected REST behavior and HTTP semantics.

I’d be happy to help contribute a PR or test enhancement around this behavior!

Thanks again for the fantastic work you all do 🙌

@browniebroke
Copy link
Member

browniebroke commented May 13, 2025

@sanyamk23 this answer looks mostly LLM-generated. Please be mindful about about your generative AI use. In particular, please avoid generating extra-verbose response that don't add much to the discussion (more than 50% of your message repeats content from earlier messages). Also, please ensure your code snippets are wrpped in code fences.

PS: we don't have an official policy but I found the Python dev guide's a reasonable starting point

@tomchristie
Copy link
Member

In my viewpoint Bruno is being exceptionally generous here.
It is not acceptable for our issues to be spammed in this way.

@encode encode temporarily blocked TooMuchInLove May 15, 2025
@encode encode temporarily blocked sanyamk23 May 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants