2828import java .util .LinkedHashMap ;
2929import java .util .List ;
3030import java .util .Map ;
31+ import java .util .Objects ;
3132
3233import org .jspecify .annotations .Nullable ;
3334
@@ -485,9 +486,18 @@ private void writeMultipart(
485486 outputMessage .getHeaders ().setContentType (contentType );
486487
487488 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+ }
491501 });
492502 }
493503 else {
@@ -496,6 +506,35 @@ private void writeMultipart(
496506 }
497507 }
498508
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+
499538 /**
500539 * When {@link #setMultipartCharset(Charset)} is configured (i.e. RFC 2047,
501540 * {@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
521560 @ SuppressWarnings ("unchecked" )
522561 private void writePart (String name , HttpEntity <?> partEntity , OutputStream os ) throws IOException {
523562 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 );
528564 HttpHeaders partHeaders = partEntity .getHeaders ();
529565 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 );
546578 }
579+ ((HttpMessageConverter <Object >) converter ).write (partBody , partContentType , multipartMessage );
580+ return ;
547581 }
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 () + "]" );
550584 }
551585
552586 /**
0 commit comments