feat: create streamable multipart body encoder#3414
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The third PR in a series related to #2734.
Introduces
feign.form.MultipartFormEncoder— a new, fully streamable encoder formultipart/form-datarequests. Unlike the existingFormEncoder, it writes each part directly to the underlyingOutputStreamwithout buffering the whole payload in memory, making it practical for large file uploads.This is an additive, non-breaking change. Existing
FormEncoderusers are not affected. See MIGRATION-v14.md for notes.Note
For maintainers: existing
FormEncoder(non-streaming) coexists in the same package as newMultipartFormEncoder(streaming). While I tried to add explicit notes toREADME.md, new users may get confused by these two encoders as well asfeign.form.FormData(existing) vsfeign.form.multipart.FormData(new) container classes. I decided not to remove any existing encoders so that this PR (a) becomes non-breaking and (b) does not introduceUrlencodedFormEncoderwhich is out of #2734 scope.If you prefer to remove the existing
FormEncoderso that the users don't get confused, then this PR will become breaking. The same goes tofeign.form.spring.SpringFormEncoder&feign.form.spring.MultipartFileEncoder.What's new
feign.form.MultipartFormEncoderThe top-level encoder. It intercepts requests whose
Content-Typeismultipart/form-dataand serialises all parameters as a streamed multipart body. Everything else is forwarded to a configurable delegate encoder (DefaultEncoderby default).Constructor / builder options:
new MultipartFormEncoder()new MultipartFormEncoder(Encoder delegate)MultipartFormEncoder.builder()feign.form.multipart.FormData<T>A wrapper that lets callers supply an explicit
filenameand/orcontentTypefor any single part, without changing the method signature:Built-in part type support
byte[]java.io.FileRequest.PathBody; filename and MIME type inferred from the file namejava.nio.file.PathFile;application/octet-streamfallback when MIME type cannot be guessedjava.io.InputStreamRequest.InputStreamBodyFormData<?>byte[])Iterable<?>toString()fallback (same as legacyFormEncoder)contentTypeEncoder(e.g.JacksonEncoder,Gson3Encoder)feign.form.spring.MultipartFileEncoder(new,form-springmodule)Streams Spring's
MultipartFiledirectly to the wire without loading the file into abyte[]:Architecture
The encoder is built around a chain-of-responsibility pattern with two orthogonal SPI layers:
Both the resolver list and the encoder list are mutable before construction (
Consumer<List<PartResolver>>/Consumer<List<PartEncoder>>), so callers can prepend, append, or replace entries without subclassing.MultipartFormBodyImplements
Request.Bodyand writes the RFC 7578 wire format directly to theOutputStream:contentLength()is accurate when all parts report a known size (soContent-Lengthis set precisely); propagates-1when any part is unknown-length (e.g.InputStream).isRepeatable()delegates to all parts.API additions
feign.codec.Encoderfeign.codec.JsonEncoder/feign.codec.XmlEncoderTwo new marker sub-interfaces that implement
supports()with appropriate content-type regexes (e.g.\w+/(\w+\+)?json.*). Existing encoder implementations have been updated to implement the right interface:feign.json.JsonEncoderfeign.codec.JsonEncoderJacksonJrEncoderfeign.codec.JsonEncoderJacksonJaxbJsonEncoderfeign.codec.JsonEncoderJAXBEncoder(javax & jakarta)feign.codec.XmlEncoderSOAPEncoder(javax & jakarta)feign.codec.XmlEncoderThis is the mechanism by which
DelegatingPartEncoderknows which encoder to invoke for a part annotated withcontentType("application/json").Minor
Request.BodyfixesPathBodyandInputStreamBody— addedequals()/hashCode().PathBody.toString()— fixed whitespace:(N bytes)→(N bytes).BytesBody.writeTo()— removed redundantObjects.requireNonNullonoutputStream(the contract already guarantees non-null).Util.CONTENT_TYPEAdded a public
"Content-Type"string constant to eliminate scattered inline literals.Tests
StreamingMultipartFormTestString,String[],byte[],File(text, octet-stream),Path,InputStream,List<String>,FormData<Movie>(JSON viaJacksonEncoder), filename + content-type override viaFormDataSpringStreamingMultipartFormTestMultipartFilestreamed viaMultipartFileEncoderBoth use WireMock 3.13.2, added as a test-scoped dependency in
form/pom.xmlandform-spring/pom.xml.