Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer people’s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer people’s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

Sorry, you do not have permission to ask a question, You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please type your username.

Please type your E-Mail.

Please choose an appropriate title for the post.

Please choose the appropriate section so your post can be easily searched.

Please choose suitable Keywords Ex: post, video.

Browse

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Navigation

  • Home
  • About Us
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask a Question
  • Home
  • About Us
  • Contact Us
Home/ Questions/Q 3006

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Latest Questions

Author
  • 61k
Author
Asked: November 26, 20242024-11-26T10:42:11+00:00 2024-11-26T10:42:11+00:00

Idempotence and POST Requests in Django

  • 61k

I have a confession to make. I should have listened to all of the articles and StackOverflow posts. I shouldn't have been so naive. But, there I was, staring at my app's poorly formatted logs trying to figure out how my users were causing my app to behave unexpectedly. I confess, that I should have taken to heart the advice that if there is a way for your users to break your app, they will.

Things to know

I have been developing web apps professionally for just over a year and have been going through the self-taught developer journey for about three years. I decided to learn Python and chose to work with the Django framework. At my current role, I create web apps to help with operational tasks. The web app who is the main subject of this article was built to help ease the new employee onboarding and set up process. Specifically, the problem was coming from the 'new employee form.'

The problem

The problem I ran into was ultimately a succinct one, thankfully. It came to my attention when users were confused why there were duplicates of the same object, in my case, duplicate employee profiles. When entering Jane Smith into the app, for example, her information was being duplicated and two instances of Jane were created in the database.

After looking through the logs and running a few tests I found out that the problem was POST requests were being sent twice, likely due to users double clicking the submit button. It is something that I considered would happen, but I decided to move forward with developing the rest of the app and would come back to later once the initial product was released.

In hindsight, it was a lack of understanding of POST requests that lead to my choice to shelf that concern. I was actually more concerned that users would try to navigate back to a previously submitted form to make edits than I was about what I assumed were harmless double clicks. Silly boy…

Considerations

After googling around the internet, I learned more about a concept called idempotence and why it is important to consider when your app is making POST requests.

At the time of working through this problem, I hadn't even begun to sniff JavaScript, let alone try and write a script that would keep a user from double clicking a submit button (I just learned that two techniques I could have deployed are called debouncing and throttling. Neat!). Another JS option I considered was giving the user some indication that the post request had successfully been initiated, like a loading spinner. Currently, the user doesn't get any help from the app letting them know that the POST request is working.

Solution pt.1

Since I needed to deploy a solution quicky and I couldn't use JS, my remaining option was to develop a solution within the 'view' of my MVC app. There didn't seem to be any obvious native Django or third-party solutions, so I would have to build something myself. Rock and Roll!

After searching for how other developers implemented idempotent POST requests, my initial idea was to send a unique identifier with each post request. This UID would be stored in a new table in my database. Each POST request from the new employee form would include a UID that was created during the POST request, and it would be checked against the database. If the UID was already present, the POST request wouldn't be allowed to finish and the user would be redirected to a different page. In the situation of a double click, each request would contain the same information, including matching UIDs. The second, errant request wouldn't be allowed to finish since a matching UID had just been added to the DB. That was the theory at least.

The solution worked, but it was far from elegant and seemed kinda hacky. I planned to setup a cron job to clean the UID table periodically. But in the meantime, I didn't want to accidently create duplicate UIDs. Random numbers wouldn't work for this solution as there remained a small chance false positives could happen. I decided to use Python 's built-in hashlib library to create a hash that was based on the employee's information, like their name and location. But what happens if the one location hires two people with the same name? That would create a false positive for my idempotent check. So, I also added the date and time to the string to be hashed. It worked. But it was gross.

Solution pt.2

While my duct taped solution was in place, I quickly got to work on a better patch. Immediately, I saw that I had multiple places where my POST requests were not idempotent. Any good solution should be able to quickly be deployed across my entire app. I take advantage of Django's class-based views and also write custom function-based views. My solution needs to work for both.

I decided to best place to start was to see how Django's maintainers approached similar problems and started digging through the documentation and code base for their built-in CSRF (SEA SURF!!!) protection.

Using that as a jumping off point, I decided to implement the following:

  1. Add a token, similar to Django' CSRF token, to each POST request.
  2. Grab token from the request and check if it is in the current session
  3. If not, proceed with processing the request and also add the token to the session.
  4. If the token is present in the session, redirect the user*.

*Deciding where to redirect the user was a hard decision to make. Where do I send the user if the post is not idempotent? Do I give them an error message? Do I act like nothing happened? If they double clicked the submit button an error message may not make sense, because technically the post request could have worked fine. It could cause confusion. I ultimately decided to make this an error that handles itself silently. Do you have other suggestions? Let me hear 'em!

(I'll explain the code in small pieces as I go along. The full code in one chunk is below.)

In more specific terms, both my custom mix-in for class-based views and my custom class for function-based views do the same thing:

  1. Custom Middleware checks to see if the session contains the idempotent token key. If not, it creates one and sets the value to an empty list. This happens on every request/response.
# middleware.py from django.shortcuts import render from django.contrib.sessions.models import Session import random  def idempotent_post_middleware(get_response):      def middleware(request):         if not request.session.__contains__('idempo_token'):             request.session.__setitem__('idempo_token', [])          response = get_response(request)          return response      return middleware 
Enter fullscreen mode Exit fullscreen mode

  1. Next, I needed to create and pass a token to the template. I use of Django's built-in template engine. Since everything is session based, I can use a random number for my UID. A random number is assigned to the new employee form on a GET request.
# class-based view # IdempotentMixin class IdempotentMixin:     idempo_redirect = None      def get_context_data(self, **kwargs):         context = super().get_context_data(**kwargs)         idempo_token_list = self.request.session.__getitem__('idempo_token')         token = random.randrange(999999)         while token in idempo_token_list:             token = random.randrange(999999)         context['idempo_token'] = token         return context 
Enter fullscreen mode Exit fullscreen mode

# function-based view # idempotent_helper.py def create_token(self):          idempo_token_list = self.request.session.__getitem__('idempo_token')         token = random.randrange(999999)         while token in idempo_token_list:             token = random.randrange(999999)         self.token = token 
Enter fullscreen mode Exit fullscreen mode

  1. Next I needed to pass this token to the template. To be honest, I really wanted to match how Django built their CSRF solution. All the developer needs to do is add the template tag. Even for function-based views, you don't have to explicitly pass the CSRF token to the view's context, it is just made available. I wasn't able to fully understand how they were doing this, so within function-based views the idempotent token must be explicitly passed. Class-based views, though, only need to extend the IdempotentMixin.
# class-based view example class YourViewClass(IdempotentMixin, CreateView): # Tell the mixin where you want to redirect users to on duplicate requests idempo-redirect='some_other_page' 
Enter fullscreen mode Exit fullscreen mode

Function based views tripped me up a little and show me how much I still have to learn about python and OOP. Everything works well, but I am not sure if my implementation is correct or best practice. Maybe you have some suggestions?

# function-based view example def your_function_view(request, pk):      idempo = IdempotentHelper(request, os.environ, redirect_to='some_other_page')     if idempo.bad_request:         return idempo.redirect ... # more of your functions code  return(request, 'a_very_good_template.html',{ # your other context items 'idempo_token':idempo.token } 
Enter fullscreen mode Exit fullscreen mode

  1. I built a custom template tag to easily add the token as a hidden field underneath the form's CSRF tag.
# custom_template_tags.py @register.simple_tag(takes_context=True) def idempotent_token(context):     token = context['idempo_token']     html = format_html('<input type="hidden" name="idempo_token" value="{}">', token)     return html 
Enter fullscreen mode Exit fullscreen mode

# a_very_good_template.html <form method="post">     {% csrf_token %}     {% idempotent_token %}     ... </form> 
Enter fullscreen mode Exit fullscreen mode

The project's full code:

# custom template tag @register.simple_tag(takes_context=True) def idempotent_token(context):     token = context['idempo_token']     html = format_html('<input type="hidden" name="idempo_token" value="{}">', token)     return html  ###  # custom middleware from django.shortcuts import render from django.contrib.sessions.models import Session import random  def idempotent_post_middleware(get_response):      def middleware(request):         if not request.session.__contains__('idempo_token'):             request.session.__setitem__('idempo_token', [])          response = get_response(request)          return response      return middleware  ###  # custom mixin for class-based views from django.http import HttpResponseRedirect from django.urls import reverse import os  import random  class IdempotentMixin:     idempo_redirect = None      def get_context_data(self, **kwargs):         context = super().get_context_data(**kwargs)         idempo_token_list = self.request.session.__getitem__('idempo_token')         token = random.randrange(999999)         while token in idempo_token_list:             token = random.randrange(999999)         context['idempo_token'] = token         return context      def post(self, request, *args, **kwargs):                if os.environ.get('test') == 'true':             return super().post(self, request, *args, **kwargs)         else:              idempo_token_list = self.request.session.__getitem__('idempo_token')             token = self.request.POST['idempo_token']             if str(token) in idempo_token_list:                 return HttpResponseRedirect(reverse(self.get_idempo_redirect()))             else:                 idempo_token_list.append(token)                 self.request.session.__setitem__('idempo_token', idempo_token_list)             return super().post(self, request, *args, **kwargs)      def get_idempo_redirect(self):         if not self.idempo_redirect:             return 'home'         else:             return self.idempo_redirect  ###  # Class for use with function-based views import random from django.http import HttpResponseRedirect from django.urls import reverse from django.shortcuts import render  class IdempotentHelper:      def __init__(self, request, environment, redirect_to=None):         self.token = None         self.request = request         self.environment = environment         self.test_name = 'test'         self.bad_request = False         self.redirect = None         self.redirect_to = redirect_to          self.create_token()         self.idempotent_check()      def create_token(self):          idempo_token_list = self.request.session.__getitem__('idempo_token')         token = random.randrange(999999)         while token in idempo_token_list:             token = random.randrange(999999)         self.token = token      def idempotent_check(self):         if self.request.method == 'POST':                         if self.environment.get(self.test_name) == 'true':                 pass             else:                 idempo_token_list = self.request.session.__getitem__('idempo_token')                 token = self.request.POST['idempo_token']                 if not token:                     self.bad_request = True                     exception = 'There appears to be an issue with the Idempotent token. Please try your request again. If the problem persists contact easyHUB admin.'                     self.redirect = render(self.request, '403.html',{'exception': exception}, status=403)                 if str(token) in idempo_token_list:                     self.bad_request = True                     self.redirect = HttpResponseRedirect(reverse(self.get_redirect_url()))                 else:                     idempo_token_list.append(token)                     self.request.session.__setitem__('idempo_token', idempo_token_list)      def get_redirect_url(self):         if not self.redirect_to:             return 'home'         else:             return self.redirect_to  
Enter fullscreen mode Exit fullscreen mode

Conclusion

I like the final solutyion I came up with, but it does have a ton of room for improvement.

  • I like if it acted even closer to how Django's native CSRF protection worked, especially for function-based views.
  • I would like to explore finding a better way to either log that the a post request didn't pass the idempotent check or alert the user that an error happened, but their post still worked.
  • I wonder if the custom middleware was needed. Maybe the token key within the session could be created within both the mixin and the class.
  • Maybe the solution for function-based views could be refactored into a decorator. I am still working on understanding and creating decorators, but that might be a more elegant solution.

What do you think needs to be fixed here? I am far from an expert in Django, Python, web development and basically anything else, so I welcome all comments!

Either way, the solution works and it keeps my users from creating multiple instances. Let's see what else they find to break!

djangowebdev
  • 0 0 Answers
  • 0 Views
  • 0 Followers
  • 0
Share
  • Facebook
  • Report

Leave an answer
Cancel reply

You must login to add an answer.

Forgot Password?

Need An Account, Sign Up Here

Sidebar

Ask A Question

Stats

  • Questions 4k
  • Answers 0
  • Best Answers 0
  • Users 2k
  • Popular
  • Answers
  • Author

    ES6 - A beginners guide - Template Literals

    • 0 Answers
  • Author

    Understanding Higher Order Functions in JavaScript.

    • 0 Answers
  • Author

    Build a custom video chat app with Daily and Vue.js

    • 0 Answers

Top Members

Samantha Carter

Samantha Carter

  • 0 Questions
  • 20 Points
Begginer
Ella Lewis

Ella Lewis

  • 0 Questions
  • 20 Points
Begginer
Isaac Anderson

Isaac Anderson

  • 0 Questions
  • 20 Points
Begginer

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore, ask, and connect. Join our vibrant Q&A community today!

About Us

  • About Us
  • Contact Us
  • All Users

Legal Stuff

  • Terms of Use
  • Privacy Policy
  • Cookie Policy

Help

  • Knowledge Base
  • Support

Follow

© 2022 Querify Question. All Rights Reserved

Insert/edit link

Enter the destination URL

Or link to existing content

    No search term specified. Showing recent items. Search or use up and down arrow keys to select an item.