]> source.dussan.org Git - jquery.git/commitdiff
Ajax: Support binary data (including FormData)
authorMichał Gołębiowski-Owczarek <m.goleb@gmail.com>
Wed, 1 Feb 2023 12:48:35 +0000 (13:48 +0100)
committerGitHub <noreply@github.com>
Wed, 1 Feb 2023 12:48:35 +0000 (13:48 +0100)
Two changes have been applied:
* prefilters are now applied before data is converted to a string;
  this allows prefilters to disable such a conversion
* a prefilter for binary data is added; it disables data conversion
  for non-string non-plain-object `data`; for `FormData` bodies, it
  removes manually-set `Content-Type` header - this is required
  as browsers need to append their own boundary to the header

Ref gh-4150
Closes gh-5197

package.json
src/ajax.js
src/ajax/binary.js [new file with mode: 0644]
src/jquery.js
test/data/mock.php
test/data/testinit.js
test/middleware-mockserver.js
test/unit/ajax.js

index 3afa4e5f175e69a9c35aeccd2a9ffaade597c318..153eb8d6a7c36660e1594b71cdce047d5d3e517f 100644 (file)
@@ -55,6 +55,7 @@
     "karma-qunit": "4.1.2",
     "karma-webkit-launcher": "2.1.0",
     "load-grunt-tasks": "5.1.0",
+    "multiparty": "4.2.3",
     "native-promise-only": "0.8.1",
     "playwright-webkit": "1.29.2",
     "promises-aplus-tests": "2.1.2",
index 36a9c9b570ee5a402de5b671612b3dc7c66c42c2..db4e301959f3132d9e8152308f6a34c7d6a9c6ad 100644 (file)
@@ -562,14 +562,14 @@ jQuery.extend( {
                        }
                }
 
+               // Apply prefilters
+               inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
                // Convert data if not already a string
                if ( s.data && s.processData && typeof s.data !== "string" ) {
                        s.data = jQuery.param( s.data, s.traditional );
                }
 
-               // Apply prefilters
-               inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
-
                // If request was aborted inside a prefilter, stop there
                if ( completed ) {
                        return jqXHR;
diff --git a/src/ajax/binary.js b/src/ajax/binary.js
new file mode 100644 (file)
index 0000000..e96661d
--- /dev/null
@@ -0,0 +1,17 @@
+import jQuery from "../core.js";
+
+import "../ajax.js";
+
+jQuery.ajaxPrefilter( function( s ) {
+
+       // Binary data needs to be passed to XHR as-is without stringification.
+       if ( typeof s.data !== "string" && !jQuery.isPlainObject( s.data ) ) {
+               s.processData = false;
+       }
+
+       // `Content-Type` for requests with `FormData` bodies needs to be set
+       // by the browser as it needs to append the `boundary` it generated.
+       if ( s.data instanceof window.FormData ) {
+               s.contentType = false;
+       }
+} );
index a0d5d364767ee19cdd7e3a5c1331029e536a9b86..d833516d45e2b685b3a81755f939b9261fda7a81 100644 (file)
@@ -23,6 +23,7 @@ import "./ajax.js";
 import "./ajax/xhr.js";
 import "./ajax/script.js";
 import "./ajax/jsonp.js";
+import "./ajax/binary.js";
 import "./ajax/load.js";
 import "./core/parseXML.js";
 import "./core/parseHTML.js";
index 0cb88cf4737bdf32c342932461f8c40e7a8ef9e5..1955f56fc2c06594224a6796a68ae7333d319ca7 100644 (file)
@@ -124,6 +124,17 @@ QUnit.assert.ok( true, "mock executed");';
                echo "$cleanCallback($text)\n";
        }
 
+       protected function formData( $req ) {
+               $prefix = 'multipart/form-data; boundary=--';
+               $contentTypeValue = $req->headers[ 'CONTENT-TYPE' ];
+               if ( substr( $contentTypeValue, 0, strlen( $prefix ) ) === $prefix ) {
+                       echo 'key1 -> ' . $_POST[ 'key1' ] . ', key2 -> ' . $_POST[ 'key2' ];
+               } else {
+                       echo 'Incorrect Content-Type: ' . $contentTypeValue .
+                               "\nExpected prefix: " . $prefix;
+               }
+       }
+
        protected function error( $req ) {
                header( 'HTTP/1.0 400 Bad Request' );
                if ( isset( $req->query['json'] ) ) {
index 6503b70a52a4441994b365f658dbcd873708ad66..906686d86ca294fc3f14d58d7dabe38809b86714 100644 (file)
@@ -174,8 +174,11 @@ function url( value ) {
 }
 
 // Ajax testing helper
-this.ajaxTest = function( title, expect, options ) {
-       QUnit.test( title, function( assert ) {
+this.ajaxTest = function( title, expect, options, wrapper ) {
+       if ( !wrapper ) {
+               wrapper = QUnit.test;
+       }
+       wrapper.call( QUnit, title, function( assert ) {
                assert.expect( expect );
                var requestOptions;
 
index 2b6970226d0ef56418f6c1655cc3acab7d801425..35e4c177858206652e771f8ea4f1c32d87f20f27 100644 (file)
@@ -3,6 +3,7 @@
 const url = require( "url" );
 const fs = require( "fs" );
 const getRawBody = require( "raw-body" );
+const multiparty = require( "multiparty" );
 
 let cspLog = "";
 
@@ -141,6 +142,19 @@ const mocks = {
                resp.writeHead( 200 );
                resp.end( `${ cleanCallback( callback ) }(${ JSON.stringify( body ) })\n` );
        },
+       formData: function( req, resp, next ) {
+               const prefix = "multipart/form-data; boundary=--";
+               const contentTypeValue = req.headers[ "content-type" ];
+               resp.writeHead( 200 );
+               if ( ( prefix || "" ).startsWith( prefix ) ) {
+                       getMultiPartContent( req ).then( function( { fields = {} } ) {
+                               resp.end( `key1 -> ${ fields.key1 }, key2 -> ${ fields.key2 }` );
+                       }, next );
+               } else {
+                       resp.end( `Incorrect Content-Type: ${ contentTypeValue
+                               }\nExpected prefix: ${ prefix }` );
+               }
+       },
        error: function( req, resp ) {
                if ( req.query.json ) {
                        resp.writeHead( 400, { "content-type": "application/json" } );
@@ -363,4 +377,18 @@ function getBody( req ) {
                } );
 }
 
+function getMultiPartContent( req ) {
+       return new Promise( function( resolve ) {
+               if ( req.method !== "POST" ) {
+                       resolve( "" );
+                       return;
+               }
+
+               const form = new multiparty.Form();
+               form.parse( req, function( _err, fields, files ) {
+                       resolve( { fields, files } );
+               } );
+       } );
+}
+
 module.exports = MockserverMiddlewareFactory;
index fec1d956580f052f615070857076d32377476285..7ecedc212196ce2dba206e688ef663284535bda6 100644 (file)
@@ -3105,4 +3105,47 @@ if ( typeof window.ArrayBuffer === "undefined" || typeof new XMLHttpRequest().re
                assert.ok( jQuery.active === 0, "ajax active counter should be zero: " + jQuery.active );
        } );
 
+       ajaxTest( "jQuery.ajax() - FormData", 1, function( assert ) {
+               var formData = new FormData();
+               formData.append( "key1", "value1" );
+               formData.append( "key2", "value2" );
+
+               return {
+                       url: url( "mock.php?action=formData" ),
+                       method: "post",
+                       data: formData,
+                       success: function( data ) {
+                               assert.strictEqual( data, "key1 -> value1, key2 -> value2",
+                                       "FormData sent correctly" );
+                       }
+               };
+       } );
+
+       ajaxTest( "jQuery.ajax() - URLSearchParams", 1, function( assert ) {
+               var urlSearchParams = new URLSearchParams();
+               urlSearchParams.append( "name", "peter" );
+
+               return {
+                       url: url( "mock.php?action=name" ),
+                       method: "post",
+                       data: urlSearchParams,
+                       success: function( data ) {
+                               assert.strictEqual( data, "pan", "URLSearchParams sent correctly" );
+                       }
+               };
+       }, QUnit.testUnlessIE );
+
+       ajaxTest( "jQuery.ajax() - Blob", 1, function( assert ) {
+               var blob = new Blob( [ "name=peter" ], { type: "text/plain" } );
+
+               return {
+                       url: url( "mock.php?action=name" ),
+                       method: "post",
+                       data: blob,
+                       success: function( data ) {
+                               assert.strictEqual( data, "pan", "Blob sent correctly" );
+                       }
+               };
+       } );
+
 } )();