500 server error with Django Rest Framework

I am using Django/DRF with Djoser and djangorestframework-simplejwt to create an API for full authentication including signup, login, activation, forgot password and reset password.

I followed along with this YT tutorial

For some reason, when I send a POST request in Postman to localhost:8000/api/users/ I am getting this error and I have no idea why at this point, django.db.utils.DatabaseError: Save with update_fields did not affect any rows.

I'm not using SQLite but an actual Postgres db on localhost. I've tried changing user.save(self._db) to just user.save(), same error. I've updated Django to 5x. Django is the only one with a significant update compared to the tutorial, he uses Django 4x.

I did move some of the original model code to a managers.py file based on this testdriven.io tutorial

I've been able to run python manage.py runserver with no errors after doing so.

It doesn't seem to be any code related to the tutorial but something with the python packages...

Here is the error from the cli:

[24/Jan/2025 16:40:07] "POST /api/users/ HTTP/1.1" 500 138392
Bad Request: /api/users/
[24/Jan/2025 17:05:47] "POST /api/users/ HTTP/1.1" 400 62
Internal Server Error: /api/users/
Traceback (most recent call last):
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/django/views/decorators/csrf.py", line 65, in _view_wrapper
    return view_func(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/rest_framework/viewsets.py", line 124, in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/rest_framework/views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
    raise exc

  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/rest_framework/mixins.py", line 19, in create
    self.perform_create(serializer)
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/djoser/views.py", line 134, in perform_create
    user = serializer.save(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/rest_framework/serializers.py", line 208, in save
    self.instance = self.create(validated_data)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/djoser/serializers.py", line 40, in create
    user = self.perform_create(validated_data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/djoser/serializers.py", line 51, in perform_create
    user.save(update_fields=["is_active"])
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/django/contrib/auth/base_user.py", line 62, in save
    super().save(*args, **kwargs)
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/django/db/models/base.py", line 892, in save
    self.save_base(
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/django/db/models/base.py", line 998, in save_base
    updated = self._save_table(
              ^^^^^^^^^^^^^^^^^
  File "/home/da/projects/full_auth_api/full_auth/backend/venv/lib/python3.12/site-packages/django/db/models/base.py", line 1136, in _save_table
    raise DatabaseError("Save with update_fields did not affect any rows.")
django.db.utils.DatabaseError: Save with update_fields did not affect any rows.
[24/Jan/2025 17:05:56] "POST /api/users/ HTTP/1.1" 500 138392

My users/models.py:

from django.contrib.postgres.functions import RandomUUID
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import (
    AbstractBaseUser,
    PermissionsMixin
)

from .managers import UserAccountManager
from django.utils.translation import gettext_lazy as _


class UserAccount(AbstractBaseUser, PermissionsMixin):
    user_id = models.UUIDField(primary_key=True, default=RandomUUID, editable=False)
    first_name = models.CharField(_('first_name'), max_length=255)
    last_name = models.CharField(_('last_name'), max_length=255)
    email = models.EmailField(unique=True, max_length=255)
    title = models.CharField(_('title'), max_length=55, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)
    date_joined = models.DateTimeField(default=timezone.now)

    objects = UserAccountManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['first_name', 'last_name']

    def __str__(self):
        return self.email

My users/managers.py:

from django.contrib.auth.models import BaseUserManager
from django.utils.translation import gettext_lazy as _
from django.db import models


class UserAccountManager(BaseUserManager):
    def create_user(self, email, password=None, **kwargs):
        """
        Creates and saves a User with the given email, date of
        birth and password.
        """
        if not email:
            raise ValueError(_('Users must have an email address'))

        email = self.normalize_email(email)
        email = email.lower()

        user = self.model(
            email=email,
            **kwargs
        )

        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password=None, **kwargs):
        """
        Creates and saves a superuser with the given email, date of
        birth and password.
        """
        user = self.create_user(
            email,
            password=password,
            **kwargs
        )

        user.is_staff = True
        user.is_superuser = True
        user.save()
        return user

In the tutorial he goes over how to use HTTP-ONLY cookies to manage the tokens, we put that code in authentication.py

And this is the users/authentication.py:

from django.conf import settings
from rest_framework_simplejwt.authentication import JWTAuthentication


    class CustomJWTAuthentication(JWTAuthentication):
        def authenticate(self, request):
            try:
                header = self.get_header(request)
    
                if header is None:
                    raw_token = request.COOKIES.get(settings.AUTH_COOKIE)
                else:
                    raw_token = self.get_raw_token(header)
    
                if raw_token is None:
                    return None
    
                validated_token = self.get_validated_token(raw_token)
    
                return self.get_user(validated_token), validated_token
            except:
                return None

If anyone needs more code, just let me know. Again I'm not sure why I'm getting this 500 server error that won't let data be saved to the database?

As I understand it, this is caused by an interaction between one of your dependencies, djoser, and the isolation level you have set in your database.

Let's start by explaining what the "Save with update_fields did not affect any rows." error relates to. This error means that Django attempted to update a database row to match the state of an ORM object. The update_fields part acts as a performance optimization: it assumes that the database row is mostly created, and only issues UPDATE ... SET ... statements corresponding to the values specified in update_fields. In this case, it's trying to set is_active. However, when it tries to set this field, it finds that zero rows match the primary key of the object that was just created.

Why is it trying to update is_active? Here's the block of code that explains this:

class UserCreateMixin:
    # ...
    def perform_create(self, validated_data):
        with transaction.atomic():
            user = User.objects.create_user(**validated_data)
            if settings.SEND_ACTIVATION_EMAIL:
                user.is_active = False
                user.save(update_fields=["is_active"])
        return user

https://github.com/sunscrapers/djoser/blob/2.3.1/djoser/serializers.py#L46

When it is creating a user, it performs the following steps:

  1. Start a transaction. This makes sure that other programs won't see a partially created user.
  2. Create user using data from serializer.
  3. If we have activation emails turned on, mark user as inactive. (This is why the transaction is required - we don't want to create a user that is allowed to log in, then remove their ability to log in. That would allow users to bypass email verification for a brief period of time.)
  4. Save the is_active field to the database.
  5. Close the transaction, committing both writes at once.

This gives us Workaround #1: if you turn off settings.SEND_ACTIVATION_EMAIL, the problem line will never run, and so you'll never get that error.

This code is implicitly assuming that the database allows you to read your own writes within a transaction.

What does Postgres do by default, here?

Read Committed is the default isolation level in PostgreSQL. When a transaction uses this isolation level, a SELECT query (without a FOR UPDATE/SHARE clause) sees only data committed before the query began; it never sees either uncommitted data or changes committed by concurrent transactions during the query's execution.

https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED

Because the user creation happens during the transaction, it is not visible to the query afterwards that checks if the object is still there.

This gives us Workaround #2: if you change Postgres's isolation level to SERIALIZABLE, then reads within a transaction will be able to see their writes. You can read how to set this here, and you should probably read the postgres docs about SERIALIZABLE, as this setting has some significant performance consequences.

To my mind, though, the best solution would be if djoser didn't make this assumption about the database's isolation level.

For example, UserCreateMixin.perform_create() could be modified to no longer require a transaction. This seems like the most straightforward solution.

Warning: this code is untested.

    def perform_create(self, validated_data):
        if settings.SEND_ACTIVATION_EMAIL:
            validated_data['is_active'] = False
        user = User.objects.create_user(**validated_data)
        return user

You'll likely want to read their contribution guide to learn how to install djoser in a way you can modify.

Вернуться на верх