@@ -283,7 +283,7 @@ public static function withSetBody(
283283
284284 if (preg_match ('/^multipart\/form-data/ ' , $ contentType )) {
285285 [$ boundary , $ gen ] = self ::encodeMultipartStreaming ($ body );
286- $ encoded = implode ('' , iterator_to_array ($ gen ));
286+ $ encoded = implode ('' , iterator_to_array ($ gen, preserve_keys: false ));
287287 $ stream = $ factory ->createStream ($ encoded );
288288
289289 /** @var RequestInterface */
@@ -447,11 +447,18 @@ private static function writeMultipartContent(
447447 ): \Generator {
448448 $ contentLine = "Content-Type: %s \r\n\r\n" ;
449449
450- if (is_resource ($ val )) {
451- yield sprintf ($ contentLine , $ contentType ?? 'application/octet-stream ' );
452- while (!feof ($ val )) {
453- if ($ read = fread ($ val , length: self ::BUF_SIZE )) {
454- yield $ read ;
450+ if ($ val instanceof FileParam) {
451+ $ ct = $ val ->contentType ?? $ contentType ;
452+
453+ yield sprintf ($ contentLine , $ ct );
454+ $ data = $ val ->data ;
455+ if (is_string ($ data )) {
456+ yield $ data ;
457+ } else { // resource
458+ while (!feof ($ data )) {
459+ if ($ read = fread ($ data , length: self ::BUF_SIZE )) {
460+ yield $ read ;
461+ }
455462 }
456463 }
457464 } elseif (is_string ($ val ) || is_numeric ($ val ) || is_bool ($ val )) {
@@ -483,17 +490,48 @@ private static function writeMultipartChunk(
483490 yield 'Content-Disposition: form-data ' ;
484491
485492 if (!is_null ($ key )) {
486- $ name = rawurlencode ( self :: strVal ( $ key) );
493+ $ name = str_replace ([ ' " ' , "\r" , "\n" ], replace: '' , subject: $ key );
487494
488495 yield "; name= \"{$ name }\"" ;
489496 }
490497
498+ // File uploads require a filename in the Content-Disposition header,
499+ // e.g. `Content-Disposition: form-data; name="file"; filename="data.csv"`
500+ // Without this, many servers will reject the upload with a 400.
501+ if ($ val instanceof FileParam) {
502+ $ filename = str_replace (['" ' , "\r" , "\n" ], replace: '' , subject: $ val ->filename );
503+
504+ yield "; filename= \"{$ filename }\"" ;
505+ }
506+
491507 yield "\r\n" ;
492508 foreach (self ::writeMultipartContent ($ val , closing: $ closing ) as $ chunk ) {
493509 yield $ chunk ;
494510 }
495511 }
496512
513+ /**
514+ * Expands list arrays into separate multipart parts, applying the configured array key format.
515+ *
516+ * @param list<callable> $closing
517+ *
518+ * @return \Generator<string>
519+ */
520+ private static function writeMultipartField (
521+ string $ boundary ,
522+ ?string $ key ,
523+ mixed $ val ,
524+ array &$ closing
525+ ): \Generator {
526+ if (is_array ($ val ) && array_is_list ($ val )) {
527+ foreach ($ val as $ item ) {
528+ yield from self ::writeMultipartField (boundary: $ boundary , key: $ key , val: $ item , closing: $ closing );
529+ }
530+ } else {
531+ yield from self ::writeMultipartChunk (boundary: $ boundary , key: $ key , val: $ val , closing: $ closing );
532+ }
533+ }
534+
497535 /**
498536 * @param bool|int|float|string|resource|\Traversable<mixed,>|array<string,mixed>|null $body
499537 *
@@ -508,14 +546,10 @@ private static function encodeMultipartStreaming(mixed $body): array
508546 try {
509547 if (is_array ($ body ) || is_object ($ body )) {
510548 foreach ((array ) $ body as $ key => $ val ) {
511- foreach (static ::writeMultipartChunk (boundary: $ boundary , key: $ key , val: $ val , closing: $ closing ) as $ chunk ) {
512- yield $ chunk ;
513- }
549+ yield from static ::writeMultipartField (boundary: $ boundary , key: $ key , val: $ val , closing: $ closing );
514550 }
515551 } else {
516- foreach (static ::writeMultipartChunk (boundary: $ boundary , key: null , val: $ body , closing: $ closing ) as $ chunk ) {
517- yield $ chunk ;
518- }
552+ yield from static ::writeMultipartField (boundary: $ boundary , key: null , val: $ body , closing: $ closing );
519553 }
520554
521555 yield "-- {$ boundary }-- \r\n" ;
0 commit comments