Redirecting With POST Requests

This post will cover how to replace a HTTP 300 redirect with a POST request.

The Solution

The solution is to implement a shortcut to render a simple response with a form that automatically submits the form to the URL you’d like to navigate the user to. Please note that this will require the browser to have JavaScript enabled.


from django.http import HttpRequest
from django.shortcuts import render
from django.urls import reverse

def post_redirect(request: HttpRequest, redirect_to: str, include_csrf: bool, **params):
    """Shortcut function for returning a POST redirect response"""
    return render(request, "post_direct.html", {
        "include_csrf": include_csrf,
        "redirect_to": redirect_to,

# Example view
def example(request):
    return post_redirect(request, redirect_to=reverse("other_url"), include_csrf=True)

Template post_direct.html

<!DOCTYPE html>
<html lang="">
  <meta charset="utf-8" />
    <!-- Put your redirecting notice here if needed -->
  <form method="post">
    {% if include_csrf %}{% csrf token %}{% endif %}
    {% for name, value in params.items %}
        <input name="{{ name }}" value="{{ value }}" type="hidden" />
    {% endfor %}

And that’s it! You can now do a redirect using POST requests.

But Why?

A good question is, “why would you want to do this?” I have two reasonable scenarios.

  1. Your application operates entirely within an iframe, and browsers are killing third-party cookies
  2. You want to redirect the user with a sensitive value without using cookies or query string

For me, it was case number one. Our application has two different users. One is the standard Django auth model. The other is simply another model. The application technically has two modes of authentication as well. One being internal members using the Django authentication system and the other users authenticate through a form of OAuth, resulting in a separate authentication token. This secondary user only uses the application through an iframe.

That iframe constraint is problematic these days. All cookies in an iframe are third-party cookies. Some browsers such as Safari allow third-party cookies if you browse to the domain of the third-party cookie. This doesn’t help us, since part of our marketing is that we seamlessly integrate into the user’s application. To work around that, we need the user to pass a token in every request without using cookies. We found three ways to pass the token to the browser:

  1. The query string
  2. The header1
  3. The body by only using POST requests

Option 1 is straight out because of the security risks of using the query string. Option 2 is only possible with AJAX requests, which we have made generously used. That leaves option 3 for every other case.


Some immediate drawbacks with this approach are:

  1. It requires the browser to have JavaScript enabled
  2. Sensitive values are now in the DOM where extensions can read them
  3. The back button breaks
  4. When a user refreshes any page, they will need to resubmit the form
  5. Users can no longer be redirected in the browser via window.location.href

Obviously, this solution is rife with problems and I don’t recommend it unless you absolutely need it.

Other solutions

Alternatively, you can use a Single Page Application (SPA) or to use something like HTMX, Hotwire or anything that can boost the HTML. You would eliminate all full-page POST requests and only use AJAX to interact with the server. This works because you can pass sensitive values between the server and browser using headers which are encrypted in HTTPS requests.

Final Words

This is the part where I complain more loudly. I’ve found cookies to be an integral part of web applications2. From what I’ve been seeing, the major browsers are working to remove them. While I despise the iframe element, they are useful and are a required tool for the web.

This change to remove third-party cookies from web applications is going to make some web apps less secure and give more sensitive information to browser extensions. Browser extensions have to request the ability to read your cookies. A savvy user can determine if that’s reasonable. Most, if not all, extensions require reading the content of the DOM. By forcing web applications to store sensitive information in the DOM, we make it impossible to prevent extensions from reading them.

I’d love a better solution, but I’m afraid things will be a bit bumpy for now.