pythonflaskwtformsdouble-submit-prevention

WTForms double POST submit prevention on refresh flask-wtf


I use WTForms and Flask with Flask-WTF extension

My form looks like:

class CommentForm(Form):
    body = TextAreaField('Body', [validators.Length(min=4, max=300)])
    entity_id = HiddenField('Entity ID', [validators.required()])

Jinja2 Template:

 <form method="POST" action="{{ request.url }}#comment-question" id="comment-question">
     <div>{{ comment_form.body }} <button type="submit">Submit</button></div>
     {{ comment_form.entity_id(value=question.id) }}
     {{ comment_form.hidden_tag() }}
 </form>

Rendered form:

<form method="POST" action="http://localhost:5000/answers/1/question-0#comment-question" id="comment-question">
  <div><textarea id="body" name="body"></textarea> <button type="submit">Submit</button></div>
  <input id="entity_id" name="entity_id" type="hidden" value="1">
  <div style="display:none;"><input id="csrf_token" name="csrf_token" type="hidden" value="20120507081937##ee73cc3cfc053266fef78b48cc645cbf90e8fba6"><input id="entity_id" name="entity_id" type="hidden" value=""></div>
</form>

Is it possible to prevent the double form submit on the browser refresh button click without changing the form "action" and doing redirects?


Solution

  • I don't have too much experience using WTForms or Flask, but Django class-based views prevent double-posts by redirecting after a POST, so I had assumed performing a redirect is the way to go for this sort of thing.

    One alternative is to generate a unique token and attach it to your form parameters (much like a CSRF token). Cache this value and check against it on form submission. A rather primitive example for Django can be found here.

    Edit: Sample code

    Although I would really just go with performing a redirect after a successful form submission, here's an example of generating a form token which borrows heavily from this Flask snippet on CSRF protection:

    # yourapp/views/filters.py
    
    import random
    from string import ascii_letters, digits
    
    from flask import request, session, redirect
    from yourapp import app
    
    
    def generate_form_token():
        """Sets a token to prevent double posts."""
        if '_form_token' not in session:
            form_token = \
                ''.join([random.choice(ascii_letters+digits) for i in range(32)])
            session['_form_token'] = form_token
        return session['_form_token']
    
    
    @app.before_request
    def check_form_token():
        """Checks for a valid form token in POST requests."""
        if request.method == 'POST':
            token = session.pop('_form_token', None)
            if not token or token != request.form.get('_form_token'):
                redirect(request.url)
    
    
    app.jinja_env.globals['form_token'] = generate_form_token
    

    And in your template:

    <!-- Again, I've never used WTForms so I'm not sure if this would change when using that app. -->
    <input type='hidden' name='_form_token' value='{{ form_token() }}' />
    

    Note that using the CSRF protection method in the snippet also accomplishes pretty much the same effect (although the above code performs a redirect, while the snippet returns a 403).

    But this really begs the question--if you're performing a redirect on an invalid token, why not get rid of all this complexity and just redirect on successful form submission?