Skip to content

[Bug]: Unintentional consecutive form submissionsΒ #81

@mcaskill

Description

@mcaskill

What did you expect? 🧐

I integrated Swup's Forms plugin for a contact form. I added the plugin's attributes [data-swup-form] and [data-swup-inline-form]. The form uses a POST method and has no action attribute (submits to the current page). The form includes Google reCAPTCHA. After a couple of weeks of use in production, we noticed a number of duplicate submissions in our database. We theorized that there is a demographic of our visitors that are double-clicking the form's submit button.

When you multi-click the submit form, depending on the speed of the visitor's clicks, the response time from the server, and the speed of Swup to replace the form, the visitor will be met with one of two outcomes:

  • They will see a successful form submission response; or
  • They will see an failed/invalid form submission response.

Without a CAPTCHA or one-time nonce:

  1. The visitor will mostly see a successful form submission. Unbeknownst to the visitor, multiple copies have been processed and emailed.

With a CAPTCHA or one-time nonce:

  1. The first submission is processed successfully by the server.
  2. The second submission fails on the server because the CAPTCHA token, or nonce, has already been validated/consumed by the first submission.
  3. Swup, depending on how fast the server is, replaces the form with either:
  • The successful form submission response (yay); or
  • The failed/invalid form submission response:
    1. The visitor assumes they made a mistake, or something went wrong, and tries again (possibly multi-clicking again).
    2. Rinse and repeat until the visitor either gets a successful response or abandons (unbeknownst to them, some requests were successful).

I failed to find the correct approach in Swup's documentation, and failed to assimilate all of Swup's and its Forms plugin' logic.

I naively thought I could use ignoreVisit for Swup.shouldIgnoreVisit in SwupFormsPlugin.beforeFormSubmit to block subsequent form submissions:

ignoreVisit: (url, { el } = {}) => {
	if (el?.closest('[data-no-swup]')) {
		return true;
	}

	if (el instanceof HTMLFormElement) {
		if (el.dataset.swupFormSubmitted) {
			return true;
		}

		el.dataset.swupFormSubmitted = 'true';
		return false;
	}

	return false;
}

I expected the second form submit event to be intercepted by my custom ignoreVisit function.

What actually happened? πŸ˜΅β€πŸ’«

Little did I know, SwupFormsPlugin.submitForm uses Swup.navigate which in turn uses Swup.shouldIgnoreVisit again. My custom ignoreVisit function thinks its dealing with a subsequent form submit event and tells Swup to ignore the visit and Swup calls window.location.assign which reloads the entire page instead of replacing the form (from the [data-swup-inline-form] attribute). All form submissions were being ignored.

Some how (probably exhaustion that day) I never noticed the form was reloading the entire page instead of displaying the success/invalid message. I would have sworn form submissions were being processed and I was seeing the expected responses. This was deployed to production (πŸ€¦β€β™‚οΈ) and the we ended up losing a few days worth of submissions.

I spent the day digging through Swup's logic and further exploring the documentation and eventually stumbled upon the "Create a plugin" page where I discovered I'm supposed to replace the Forms plugin's default handler:

The correct solution, ultimately is the following:

swup.hooks.replace('form:submit', (visit, args, defaultHandler) => {
	if (!args.el || !args.event) {
		return defaultHandler(visit, args);
	}

	if (!args.el.dataset?.swupFormSubmitted) {
		args.el.dataset.swupFormSubmitted = 'true';
		return defaultHandler(visit, args);
	}

	args.event?.preventDefault();
});

I must have missed these sections of information during my initial attempt because of the page's title being "Create a plugin". The "Hooks" page makes a single reference to a "default handler" and no mention that it could be replaced.

Ultimately, its my fault, and this is not much of a bug, and could be better filed as an issue in the swup/docs repository.

I'm filing it in this plugin's repository in case someone else finds themselves in my shoes.

Maybe there's something that could be done for this plugin such as adding an option to prevent consecutive submits. At the very least, improve the documentation by mentioning default handler replacements in the Hooks page and/or including the snippet above in the Forms plugin's page.

Thank you for your time,

P.S., the Replit demo I assembled, the signup.php file will return HTTP 400 half the time to give a proper idea potential fail states for visitors.

Screen capture of a Replit demonstration of Swup Forms plugin where a double-click of the form's submit button produced two different responses from the server

Swup and plugin versions πŸ“

  • swup: 4.8.1
  • @swup/forms-plugin: 3.6.0

What browsers are you seeing the problem on? 🧭

No response

Relevant log output πŸ€“

URL to minimal reproduction πŸ”—

https://replit.com/@mcaskill/Swup-Demo-Inline-Forms?v=1

Checked all these? πŸ“š

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions