28
28
import java .util .LinkedHashMap ;
29
29
import java .util .List ;
30
30
import java .util .Map ;
31
+ import java .util .Objects ;
31
32
32
33
import org .jspecify .annotations .Nullable ;
33
34
@@ -485,9 +486,18 @@ private void writeMultipart(
485
486
outputMessage .getHeaders ().setContentType (contentType );
486
487
487
488
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage ) {
488
- streamingOutputMessage .setBody (outputStream -> {
489
- writeParts (outputStream , parts , boundary );
490
- writeEnd (outputStream , boundary );
489
+ boolean repeatable = checkPartsRepeatable (parts );
490
+ streamingOutputMessage .setBody (new StreamingHttpOutputMessage .Body () {
491
+ @ Override
492
+ public void writeTo (OutputStream outputStream ) throws IOException {
493
+ FormHttpMessageConverter .this .writeParts (outputStream , parts , boundary );
494
+ writeEnd (outputStream , boundary );
495
+ }
496
+
497
+ @ Override
498
+ public boolean repeatable () {
499
+ return repeatable ;
500
+ }
491
501
});
492
502
}
493
503
else {
@@ -496,6 +506,35 @@ private void writeMultipart(
496
506
}
497
507
}
498
508
509
+ @ SuppressWarnings ({"unchecked" , "ConstantValue" })
510
+ private <T > boolean checkPartsRepeatable (MultiValueMap <String , Object > map ) {
511
+ return map .entrySet ().stream ().allMatch (e -> e .getValue ().stream ().filter (Objects ::nonNull ).allMatch (part -> {
512
+ HttpHeaders headers = null ;
513
+ Object body = part ;
514
+ if (part instanceof HttpEntity <?> entity ) {
515
+ headers = entity .getHeaders ();
516
+ body = entity .getBody ();
517
+ Assert .state (body != null , "Empty body for part '" + e .getKey () + "': " + part );
518
+ }
519
+ HttpMessageConverter <?> converter = findConverterFor (e .getKey (), headers , body );
520
+ return (converter instanceof AbstractHttpMessageConverter <?> ahmc &&
521
+ ((AbstractHttpMessageConverter <T >) ahmc ).supportsRepeatableWrites ((T ) body ));
522
+ }));
523
+ }
524
+
525
+ private @ Nullable HttpMessageConverter <?> findConverterFor (
526
+ String name , @ Nullable HttpHeaders headers , Object body ) {
527
+
528
+ Class <?> partType = body .getClass ();
529
+ MediaType contentType = (headers != null ? headers .getContentType () : null );
530
+ for (HttpMessageConverter <?> converter : this .partConverters ) {
531
+ if (converter .canWrite (partType , contentType )) {
532
+ return converter ;
533
+ }
534
+ }
535
+ return null ;
536
+ }
537
+
499
538
/**
500
539
* When {@link #setMultipartCharset(Charset)} is configured (i.e. RFC 2047,
501
540
* {@code encoded-word} syntax) we need to use ASCII for part headers, or
@@ -521,32 +560,27 @@ private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, by
521
560
@ SuppressWarnings ("unchecked" )
522
561
private void writePart (String name , HttpEntity <?> partEntity , OutputStream os ) throws IOException {
523
562
Object partBody = partEntity .getBody ();
524
- if (partBody == null ) {
525
- throw new IllegalStateException ("Empty body for part '" + name + "': " + partEntity );
526
- }
527
- Class <?> partType = partBody .getClass ();
563
+ Assert .state (partBody != null , "Empty body for part '" + name + "': " + partEntity );
528
564
HttpHeaders partHeaders = partEntity .getHeaders ();
529
565
MediaType partContentType = partHeaders .getContentType ();
530
- for (HttpMessageConverter <?> messageConverter : this .partConverters ) {
531
- if (messageConverter .canWrite (partType , partContentType )) {
532
- Charset charset = isFilenameCharsetSet () ? StandardCharsets .US_ASCII : this .charset ;
533
- HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage (os , charset );
534
- String filename = getFilename (partBody );
535
- ContentDisposition .Builder cd = ContentDisposition .formData ()
536
- .name (name );
537
- if (filename != null ) {
538
- cd .filename (filename , this .multipartCharset );
539
- }
540
- multipartMessage .getHeaders ().setContentDisposition (cd .build ());
541
- if (!partHeaders .isEmpty ()) {
542
- multipartMessage .getHeaders ().putAll (partHeaders );
543
- }
544
- ((HttpMessageConverter <Object >) messageConverter ).write (partBody , partContentType , multipartMessage );
545
- return ;
566
+ HttpMessageConverter <?> converter = findConverterFor (name , partHeaders , partBody );
567
+ if (converter != null ) {
568
+ Charset charset = isFilenameCharsetSet () ? StandardCharsets .US_ASCII : this .charset ;
569
+ HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage (os , charset );
570
+ String filename = getFilename (partBody );
571
+ ContentDisposition .Builder cd = ContentDisposition .formData ().name (name );
572
+ if (filename != null ) {
573
+ cd .filename (filename , this .multipartCharset );
574
+ }
575
+ multipartMessage .getHeaders ().setContentDisposition (cd .build ());
576
+ if (!partHeaders .isEmpty ()) {
577
+ multipartMessage .getHeaders ().putAll (partHeaders );
546
578
}
579
+ ((HttpMessageConverter <Object >) converter ).write (partBody , partContentType , multipartMessage );
580
+ return ;
547
581
}
548
- throw new HttpMessageNotWritableException ("Could not write request: no suitable HttpMessageConverter " +
549
- "found for request type [" + partType .getName () + "]" );
582
+ throw new HttpMessageNotWritableException ("Could not write request: " +
583
+ "no suitable HttpMessageConverter found for request type [" + partBody . getClass () .getName () + "]" );
550
584
}
551
585
552
586
/**
0 commit comments