Django/DRF backend not receiving complete data from POST request

Viewed 431

I'm trying to send data to my Django/DRF backend via Axios POST requests. Some of them have been working, but some of them haven't been. This snippet, for example, works fine:

      axiosInstance.post(
        `/api/notes/`,
        {
          title,
          content: serialize(editorContent),
        }
      )

This one, however, has been causing problems:

    axiosInstance.post(
      `/api/notebooks/`,
      {
        name: newNotebookName,
        user,
      }
    )

In the second snippet, the name attribute gets through to the backend, but for some reason the user attribute does not. I tried using console.log to see if user is undefined at the moment it's sent off to the backend and it isn't.

I tried to change the way I define the keys in the object I send to the backend so that they're not dynamic like in this answer and the result hasn't changed.

The user variable is a string containing an email address, like 'foobar@gmail.com'. I thought maybe Django blocked it by default for security reasons, but I split the string at the @ and tried sending the first part only and it didn't work either.

I also tried defining a new variable so that the type of user is string rather than string | undefined and it still doesn't work. The value of title is also string and it gets through to the backend (it's defined in the validated_data object my serializer sees in its create() method). So I'm at a loss as to what this could be. Here are some of my files:

axiosAPI.ts

import axios from 'axios'

const baseURL = 'http://localhost:8000/'

export const axiosInstance = axios.create({
  baseURL: baseURL,
  timeout: 5000,
  headers: {
    'xsrfHeaderName': 'X-CSRFTOKEN',
    'xrsfCookieName': 'csrftoken',
    'Authorization': localStorage.getItem('access_token') ? 'JWT ' + localStorage.getItem('access_token') : null,
    'Content-Type': 'application/json',
    'accept': 'application/json',
  }
})

axiosInstance.interceptors.response.use(
  response => response,
  error => {
    const originalRequest = error.config

    // Prevent infinite loops if login credentials invalid
    if (error.response.status === 406 && originalRequest.url === baseURL + 'auth/token/obtain') {
      window.location.href = '/login/'

      return Promise.reject(error);
    }

    // Respond to expired refresh tokens
    if (error.response.data.code === 'token_not_valid' && error.response.status === 401 && error.response.statusText === 'Unauthorized') {
      const refreshToken = localStorage.getItem('refresh_token')
      
      if (refreshToken) {
        const tokenObj = JSON.parse(atob(refreshToken.split('.')[1]))
        const currentTime = Math.ceil(Date.now() / 1000)
        
        if (tokenObj.exp > currentTime) {
          return axiosInstance
            .post('/auth/token/refresh/', { refresh: refreshToken })
            .then(response => {
              localStorage.setItem('access_token', response.data.access)
              localStorage.setItem('refresh_token', response.data.refresh)

              axiosInstance.defaults.headers['Authorization'] = 'JWT ' + response.data.access
              originalRequest.headers['Authorization'] = 'JWT ' + response.data.access

              return axiosInstance(originalRequest);
            })
            .catch(err => {
              console.log(err)
              throw err;
            })
        } else {
          console.log('Refresh token is expired.')
          window.location.href = '/login/'
        }
      } else {
        console.log('Refresh token not available.')
        window.location.href = '/login/'
      }
    }
    
    // Respond to invalid access tokens
    if (error.response.status === 401 && error.response.statusText === 'Unauthorized') {
      const refreshToken = localStorage.getItem('refresh_token')
      
      return axiosInstance
        .post('/auth/token/refresh/', { refresh: refreshToken })
        .then(response => {
          localStorage.setItem('access_token', response.data.access)
          localStorage.setItem('refresh_token', response.data.refresh)
          
          axiosInstance.defaults.headers['Authorization'] = 'JWT ' + response.data.access
          originalRequest.headers['Authorization'] = 'JWT ' + response.data.access

          return axiosInstance(originalRequest)
        }).catch(err => {
          console.log(err)
          throw err;
        })
    }
  }
)

notebooks/serializers.py

import bleach
import json

from rest_framework import serializers

from authentication.models import ErsatzNoteUser
from .models import Note, Notebook

class NoteSerializer(serializers.ModelSerializer):
  note_id = serializers.SlugField(source='id', read_only=True, required=False)
  title = serializers.JSONField(required=False)
  content = serializers.CharField(required=False)
  notebook = serializers.PrimaryKeyRelatedField(read_only=True, required=False)

  date_modified = serializers.DateField(read_only=True, required=False)
  date_created = serializers.DateField(read_only=True, required=False)

  def create(self, validated_data):
    title = json.dumps(validated_data['title'])

    # Workaround to fix a currently unpatched bug in Slate
    # that occurs when an editor's contents begin with a list
    content = validated_data['content']
    if content.startswith('<ul') or content.startswith('<ol'):
      content = '<p></p>' + content

    response_data = {
      'title': title,
      'content': content,
    }

    return Note.objects.create(**response_data)

  def update(self, instance, validated_data):
    instance.title = json.dumps(validated_data['title'])

    # See the above comment in the 'create' method
    content = validated_data['content']
    if content.startswith('<ul') or content.startswith('<ol'):
      content = '<p></p>' + content
    
    instance.content = content

    instance.save()

    return instance

  class Meta:
    model = Note
    fields = [ 'note_id', 'title', 'content', 'notebook', 'date_modified', 'date_created' ]

class NotebookSerializer(serializers.ModelSerializer):
  notebook_id = serializers.SlugField(source='id', read_only=True, required=False)
  name = serializers.CharField(max_length=64, default='')
  notes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

  def create(self, validated_data):
    # notebook_data = {
    #   'name': validated_data['name'],
    #   'user': ErsatzNoteUser.objects.get(email=validated_data['user']),
    # }
    print(validated_data)
    
    return Notebook.objects.create(name=validated_data['name'], user=ErsatzNoteUser.objects.get(email=validated_data['user']))
  
  class Meta:
    model = Notebook
    fields = [ 'notebook_id', 'name', 'notes', 'date_modified', 'date_created' ]

notebooks/views.py

import json

from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response

from .serializers import NoteSerializer, NotebookSerializer
from .models import Note, Notebook

class NoteViewSet(viewsets.ModelViewSet):
  serializer_class = NoteSerializer
  
  def get_queryset(self):
    return self.request.user.notes.all()
  
  def retrieve(self, request, *args, **kwargs):
    instance = self.get_object()
    serializer = NoteSerializer(instance)

    title = json.loads(serializer.data['title'])

    response_data = {
      'note_id': serializer.data['note_id'],
      'title': title,
      'content': serializer.data['content'],
      'notebook': serializer.data['notebook'],
      'date_modified': serializer.data['date_modified'],
      'date_created': serializer.data['date_created'],
    }

    return Response(response_data)

class NotebookViewSet(viewsets.ModelViewSet):
  serializer_class = NotebookSerializer

  def get_queryset(self):
    return self.request.user.notebooks.all()

notebooks/models.py

from django.db import models
from django.contrib.auth.models import User
from jsonfield import JSONField
from django.conf import settings

from .helpers import generate_slug

class Note(models.Model):
  """ Represents an individual note. """

  id = models.SlugField(max_length=settings.MAX_SLUG_LENGTH, primary_key=True)
  title = JSONField(null=True)
  content = models.TextField(null=True)
  notebook = models.ForeignKey('Notebook', related_name='notes', on_delete=models.CASCADE, null=True, blank=True)
  user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='notes', on_delete=models.CASCADE, null=True, blank=True)
  date_created = models.DateField(auto_now_add=True)
  date_modified = models.DateField(auto_now=True)
  
  def save(self, *args, **kwargs):
    if not self.id:
      self.id = generate_slug(self, settings.MAX_SLUG_LENGTH)
    # Temporary expedients for the sake of development
    if not self.notebook:
      self.notebook = Notebook.objects.get(id='YyOzNhMFMPtN8HM')
    super(Note, self).save(*args, **kwargs)
  
  def __str__(self):
    return self.id
  
  class Meta:
    ordering = [ '-date_modified', '-date_created', ]

class Notebook(models.Model):
  """ A collection of individual notes. """
  
  id = models.SlugField(max_length=settings.MAX_SLUG_LENGTH, primary_key=True)
  name = models.CharField(max_length=64, default='')
  user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='notebooks', on_delete=models.CASCADE)
  date_created = models.DateField(auto_now_add=True)
  date_modified = models.DateField(auto_now=True)

  def save(self, *args, **kwargs):
    if not self.id:
      self.id = generate_slug(self, settings.MAX_SLUG_LENGTH)
    super(Notebook, self).save(*args, **kwargs)

  def __str__(self):
    return self.name
  
  class Meta:
    ordering = [ '-date_modified', '-date_created', 'name' ]

Please don't hesitate to ask if you'd like further info.

EDIT: Changed NotebookViewSet so that it has a retrieve() method, the result hasn't changed:

class NotebookViewSet(viewsets.ModelViewSet):
  serializer_class = NotebookSerializer

  def get_queryset(self):
    return self.request.user.notebooks.all()
  
  def retrieve(self, request, *args, **kwargs):
    instance = self.get_object()
    serializer = NotebookSerializer(instance)

    response_data = {
      'name': serializer.data['name'],
      'user': serializer.data['user'],
    }

    return Response(response_data)
2 Answers

In your example your NotebookViewSet class does not have a retrieve method.

It turns out that under the hood, DRF considers ForeignKey fields to be read_only. Meaning it's solely to be used for representing the object, and shouldn't be part of any create or update process. According to the documentation:

Read-only fields are included in the API output, but should not be included in the input during create or update operations. Any 'read_only' fields that are incorrectly included in the serializer input will be ignored.

Source.

But this didn't matter to my database, which requires me to identify the user that corresponds to each new Notebook at the time of its creation, or else a NOT NULL constraint is violated. So I just overwrote the default ModelViewSet create() method to pass on the user string manually to my NotebookSerializer as part of context, allowing me to use it to get the corresponding user object in the database and assign it to the newly created Notebook. I went about it like this:

class NotebookViewSet(viewsets.ModelViewSet):
  serializer_class = NotebookSerializer

  def get_queryset(self):
    return self.request.user.notebooks.all()

  def perform_create(self, serializer):
    serializer.save()

  def create(self, request, *args, **kwargs):
    serializer = NotebookSerializer(data=request.data, context={ 'request': request })
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)

    return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class NotebookSerializer(serializers.ModelSerializer):
  notebook_id = serializers.SlugField(source='id', read_only=True, required=False)
  name = serializers.CharField(max_length=64, default='')
  notes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

  def create(self, validated_data):
    return Notebook.objects.create(
      user=ErsatzNoteUser.objects.get(email=self.context['request'].user),
      **validated_data
    )
  
  class Meta:
    model = Notebook
    fields = [ 'notebook_id', 'name', 'notes', 'date_modified', 'date_created' ]

That's all it took.

Related