From eabec62065ca694b48d23e4cc224ecdb2731522a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylvain=20Monn=C3=A9?= Date: Fri, 15 Jul 2022 18:52:12 +0200 Subject: [PATCH] Core: fix race condition in remote validation rules Fixes #2434 --- src/ajax.js | 17 +++++++++++------ src/core.js | 27 ++++++++++++++++++++++++++- test/methods.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/ajax.js b/src/ajax.js index fb56de4b3..5f87bed20 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -1,5 +1,6 @@ // Ajax mode: abort // usage: $.ajax({ mode: "abort"[, port: "uniqueport"]}); +// $.ajaxAbort( port ); // if mode:"abort" is used, the previous request on that port (port can be undefined) is aborted via XMLHttpRequest.abort() var pendingRequests = {}, @@ -10,9 +11,7 @@ if ( $.ajaxPrefilter ) { $.ajaxPrefilter( function( settings, _, xhr ) { var port = settings.port; if ( settings.mode === "abort" ) { - if ( pendingRequests[ port ] ) { - pendingRequests[ port ].abort(); - } + $.ajaxAbort( port ); pendingRequests[ port ] = xhr; } } ); @@ -24,12 +23,18 @@ if ( $.ajaxPrefilter ) { var mode = ( "mode" in settings ? settings : $.ajaxSettings ).mode, port = ( "port" in settings ? settings : $.ajaxSettings ).port; if ( mode === "abort" ) { - if ( pendingRequests[ port ] ) { - pendingRequests[ port ].abort(); - } + $.ajaxAbort( port ); pendingRequests[ port ] = ajax.apply( this, arguments ); return pendingRequests[ port ]; } return ajax.apply( this, arguments ); }; } + +// Abort the previous request without sending a new one +$.ajaxAbort = function( port ) { + if ( pendingRequests[ port ] ) { + pendingRequests[ port ].abort(); + delete pendingRequests[ port ]; + } +}; diff --git a/src/core.js b/src/core.js index ddb258119..923b3012b 100644 --- a/src/core.js +++ b/src/core.js @@ -756,6 +756,9 @@ $.extend( $.validator, { val = this.elementValue( element ), result, method, rule, normalizer; + // Abort any pending Ajax request from a previous call to this method. + this.abortRequest( element ); + // Prioritize the local normalizer defined for this element over the global one // if the former exists, otherwise user the global one in case it exists. if ( typeof rules.normalizer === "function" ) { @@ -1095,6 +1098,10 @@ $.extend( $.validator, { return !$.validator.methods.required.call( this, val, element ) && "dependency-mismatch"; }, + elementAjaxPort: function( element ) { + return "validate" + element.name; + }, + startRequest: function( element ) { if ( !this.pending[ element.name ] ) { this.pendingRequest++; @@ -1130,6 +1137,24 @@ $.extend( $.validator, { } }, + abortRequest: function( element ) { + var port; + + if ( this.pending[ element.name ] ) { + port = this.elementAjaxPort( element ); + $.ajaxAbort( port ); + + this.pendingRequest--; + + // Sometimes synchronization fails, make sure pendingRequest is never < 0 + if ( this.pendingRequest < 0 ) { + this.pendingRequest = 0; + } + + delete this.pending[ element.name ]; + } + }, + previousValue: function( element, method ) { method = typeof method === "string" && method || "remote"; @@ -1570,7 +1595,7 @@ $.extend( $.validator, { data[ element.name ] = value; $.ajax( $.extend( true, { mode: "abort", - port: "validate" + element.name, + port: this.elementAjaxPort( element ), dataType: "json", data: data, context: validator.currentForm, diff --git a/test/methods.js b/test/methods.js index fa49939e4..2966ec25f 100644 --- a/test/methods.js +++ b/test/methods.js @@ -801,6 +801,36 @@ QUnit.test( "Fix #697: remote validation uses wrong error messages", function( a } ); } ); +QUnit.test( "Fix #2434: race condition in remote validation rules", function( assert ) { + var e = $( "#username" ), + done1 = assert.async(), + v = $( "#userForm" ).validate( { + rules: { + username: { + required: true, + remote: { + url: "users.php" + } + } + }, + messages: { + username: { + remote: $.validator.format( "{0} in use" ) + } + } + } ); + + e.val( "Peter" ); + v.element( e ); + + e.val( "" ); + v.element( e ); + setTimeout( function() { + assert.equal( v.errorList[ 0 ].message, "This field is required." ); + done1(); + } ); +} ); + QUnit.module( "additional methods" ); QUnit.test( "phone (us)", function( assert ) {