Contents

  • The problem
  • Is it a browser issue?
  • Is it a email server issue?
  • Is it an automatic behaviour issue?
  • Creating a test project for debugging
  • Is it a data issue?
  • Conclusion

The Problem

I've been implementing the user authentication journey for my Django library app focused on fantasy books.

Recently, I've been implementing the password reset page.

Password reset link on login page

Password reset page

The front-end all looked and worked as expected, but there was one problem: every time I clicked on the password reset button, it wouldn't only send 1 email - it would send 12.

Multiple password reset emails

This is the problem that has confused me the most since I've been working on personal projects on my own.

I wanted to record my process of how I debugged it and eventually got to the bottom of what was happening.

Is it a browser-based issue?

My first thought was that something was causing the submit button to be clicked multiple times, even though I knew I was only clicking it once.

With using Django, I didn't have any JavaScript up until this point in my code. That's because I was using a lot of the automatic behaviour that comes with Django’s authentication system with its default configuration, like using django.contrib.auth.urls.

urlpatterns = [
    path("accounts/", include("django.contrib.auth.urls")
]

This includes the following URL patterns:

accounts/login/ [name='login']
accounts/logout/ [name='logout']
accounts/password_change/ [name='password_change']
accounts/password_change/done/ [name='password_change_done']
accounts/password_reset/ [name='password_reset']
accounts/password_reset/done/ [name='password_reset_done']
accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/reset/done/ [name='password_reset_complete']

By having this included in my urls.py file, I found that I didn't need write out a view for the 'password reset form' page (password_reset_form.html) to tell it to redirect to my 'password reset done' page (password_reset_done.html) after the form was submitted. This behaviour happened automatically.

As something weird clearly was going on though, I created a JavaScript file and got this to log to the console every time that the password reset button was clicked.

const passwordResetButton = document.querySelector(".password-reset-button")
if (passwordResetButton)
{
    passwordResetButton.addEventListener("click", (e) =>
    {
        console.log("password reset button clicked")
    })
}

When I clicked the password reset button, I found that the event listener was only firing once and only one message was logged to the console. This meant that my first theory that the click event listener was firing multiple times on click couldn't be right. Hmm.

Checking the POST request

Next, I decided to see how many times a POST request was being submitted from my password reset form. I wanted to check if something meant that there were 12 POST requests being sent instead of 1.

When clicking the password reset button, I could see that there was only one POST request:

"GET /accounts/login/ HTTP/1.1" 200 5318
"GET /accounts/password_reset/ HTTP/1.1" 200 4144
"POST /accounts/password_reset/ HTTP/1.1" 302 0
"GET /accounts/password_reset/done/ HTTP/1.1" 200 3717

This meant that I was still very confused about how 12 emails could be sent by 1 action.

Is it an email issue?

Next, I decided to try the theory that the problem could be related to the gmail email server. There are different settings that you can use when sending emails in Django. To send emails, I had been using:

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_USE_TLS = True
EMAIL_PORT = 587
EMAIL_HOST_USER = os.environ.get(str('EMAIL_USER'))
EMAIL_HOST_PASSWORD = os.environ.get(str('EMAIL_PASSWORD'))

Other options include having the email print to the console instead of sending an email, or having an email appear as a file inside a folder.

Printing to the console:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Saving emails to a folder:

EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
EMAIL_FILE_PATH = BASE_DIR / 'emails'

I switched to using the console method and found that, yep, there were still 12 emails printing to the console with one click. So I knew it couldn't be a gmail issue.

Is it an automatic behaviour issue?

My next port of call was thinking that this 12 email issue was related to me using Django's inbuilt authentication system. It felt strange that I hadn't specified any view logic for what should happen when a POST request was submitted from my password reset form page. I thought that, if I explicitly wrote out in the code that I wanted the password reset form page to redirect to the password reset done page when a POST request was submitted and the form was valid, then it would make sure no odd 12x looping behaviour could happen.

In hindsight, I feel that the troubleshooting steps I'd already taken had shown that this redirect was already happening correctly, but I was at a bit of a loss.

What I wrote in my views.py file looked something like this:

def password_reset_request(request):
    if request.method == "POST":
        form = PasswordResetForm(request.POST)  
        if form.is_valid():
            form.save(
                # save settings here, shortened in this snippet)
            return redirect('password_reset_done')
    else:
        form = PasswordResetForm()

    return render(request, "registration/password_reset_form.html", {"form": form})

After I made this change, I found that the behaviour was exactly the same using this custom view as it was before, so that solution was a no go.

Getting password resets to work in a separate project

At this point, I felt really stuck. I couldn't see that there was any logic in the code that meant that an action was repeating 12 times.

I decided to create a new test django project from scratch which only needed to successfully submit password resets. I didn't spend any time on styling, I just got it the password reset functionality to work.

Password reset page - test project

In getting this test project to work, I came across an issue (not really an issue, it's by design) where the password reset email only sent an email if the email address was already stored in the database.

And that's when I realised there was one debugging step I hadn't looked at - THE DATA!

Is the issue the data?

I remembered that, in testing the sign up process, I had created many (12, in fact) accounts all associated with my email address. This meant that the password reset process was working as expected - but it was sending 1 email for every account that matched the email address being typed in. The mystery was finally solved!

Conclusion

This process taught me a lot about the different steps to follow for how to debug a problem. It also reminded me of a blind spot I had when it comes to debugging Django specifically - I forgot about the User model and how many Users had the same email address because I hadn't needed to define a User model explicitly in my models.py. Django did that for me automatically.

I think it's now something I'm much less likely to overlook next time. This was also the first time I created a test project as part of the debugging process and I'll definitely keep this technique in mind for problems I can't solve through other troubleshooting steps in the future.