feat: add @Part annotation for multipart form-data request contracts#3450
feat: add @Part annotation for multipart form-data request contracts#3450yvasyliev wants to merge 5 commits into
@Part annotation for multipart form-data request contracts#3450Conversation
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
|
@yvasyliev nice! I would like to encourage that we have a clear model of the filename handling - i.e. we get the Encoder part of the puzzle also completely figured out as part of this PR. Specifically, I'm still not crystal clear on how the Encoder will handle putting the filename in the content disposition header without having multipart specific File and Path encoders. I had originally suggested using a bean expression syntax to address this - is that still on the table, or are you thinking of something different? |
|
@yvasyliev one big suggestion to consider: I think the current PR may be pushing too much responsibility to the Encoder... I think the place to handle sub-part encoding is in BuildMultipartTemplateFromArgs itself. Same with the unwrapping/unrolling/exploding collections. Here's what that would look like (I haven't compiled this, so there may be some type-os): Basically, BuildMultipartTemplateFromArgs would produce the exact MultiPartFormData object that a user would create manually if they weren't using annotations. This keeps the architecture nice and encapsulated. The multipart encoder then becomes very simple - it just adds the separator header to the request template, then it creates a Body that adds the separator and part headers, plus calls each part Body to emit its data to the output beneath each part header. If we have good separation of concerns, the user should be able to register a MultiPartFormDataEncoder in Feign config then do either of these and get the same behavior: or If the user can do either of those with the same result (and not having to jump through hoops in the back-end code to make it happen), then we will have really good encapsulation. |
|
So, Like the legacy This way, your second option becomes: // option 2
public interface FileUploadClient {
@RequestLine("POST /api/v1/upload")
void uploadForm(MultipartFormBody body);
}
//
uploadClient.uploadForm(new MultipartFormBody(
"my-own-boundary",
List.of(new PartBody(
Map.of(
"Content-Disposition", List.of("form-data; name=\"file\"; filename=\"aaaaa.xml\""),
"Content-Type", List.of("application/xml")
),
new Request.PathBody(myPath)
))
));Does it work for you? |
|
ok - I see that I may have misinterpreted the MultiPartFormData class. I don't see a MultiPartFormBody class in the PR - maybe this is why I got confused? As for concerns, here's how I look at it: The Contract is concerned with reading the interface method signatures and building a meta object. The RequestTemplateFactoryResolver is about combining the actual arguments of the method call (which can contain a Java object that represents the body) and the method meta to construct a resolved RequestTemplate. The construction of a RequestTemplate requires an Encoder to convert the Java object argument to a Body in the RequestTemplate (recognizing that this encoding may require some reading and/or setting of the request headers). My strong opinion is that Encoders are concerned only with converting a Java object into the body+headers required for the request to be sent. When we make this distinction, very good things happen to the architecture (including your original very important observation that encoders should be usable for multi-part section encoding). When the encoders try to be responsible for more than that one concern, all sorts of dependencies and limitations show up. With multi-part, the parallel between the sub-part and Request is very, very strong, and I think we can take advantage of that to really simplify the coding and make things more intuitive for users. I am pretty confident about my description of the Encoder's (limited) concern because of the following:
One thing that is certain: Encoders are designed to work with RequestTemplate. So if we want to re-use Encoders (and I think that we really, really do), then it makes a lot of sense to use RequestTemplate and resolve it from within the RequestTemplateFactoryResolver. Unfortunately, I don't think that the MultiPartFormData class as written is very helpful - you can see from my Fundamentally, I see many reasons to do this request template resolution in RequestTemplateFactoryResolver, but I can't see much benefit to trying to do any of that in the encoder. For thoroughness, here is what I think the MultiPartFormEncoder would look like - very simple, focused only on encoding a well defined MultiPartFormBody: Note that this encoder does not need access to any other encoders - because the part headers and body have already been resolved. Does that make sense? |
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
…`CHANGELOG.md` Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
|
@trumpetinc I have completed the POC with a I assume that constructing |
Co-authored-by: trumpetinc 6618744+trumpetinc@users.noreply.github.com
Summary
Another PR in the series related to #2734. Delivers the full multipart request API for Feign.
This PR introduces first-class, declarative support for
multipart/form-datarequests in Feign through a new@Partparameter annotation, backed by a dedicatedMultipartFormEncoderthat serializes the parsed parts into a streaming-capablemultipart/form-databody.This change is fully additive and non-breaking. Existing users and contracts will experience zero impact, as it introduces entirely new resolution pathways without altering standard form-encoded or single-body behaviors.
Usage Example
API Design & Naming Rationale
During background design discussions, the API contract and naming conventions were carefully weighed to ensure maximum flexibility and architectural consistency with Feign's existing codebase:
1. The
@PartAnnotation@Partinstead of@Multipartor@FormData? The term multipart is fundamentally coupled to the nature of the entire HTTP request rather than an individual parameter. Conversely, form data is specific to a single header type (Content-Disposition). We needed a name that describes the entire standalone part entity (comprising its own distinct headers, metadata, and body payload). Hence,@Partis the most precise candidate.value&headers):The
valueattribute acts as a concise alias forheaders. This elegant symmetry allows developers to seamlessly scale from a simple shorthand string (@Part("partName")) to a full raw header line or a multi-line array of distinct headers.explodeFlag:This property mirrors Feign's existing
feign.CollectionFormat#EXPLODEDconfiguration paradigm. It defines whether a collection or array should be unrolled into repeated separate parts on the wire.name/filenameattributes:An alternative design considered introducing dedicated
nameandfilenameproperties directly onto the annotation. This was rejected because it introduces unneeded implementation complexity while arbitrarily limiting the user from injecting custom headers into individual parts.2.
MultipartFormData&PartDataMultipartFormData: Named to maintain a clear conceptual bridge to the legacyMap<String, Object> formDatainstances heavily utilized throughout Feign's core engine.PartData: Extends the semantic pattern established byMultipartFormData, providing an isolated, intuitive representation of a specific runtime part payload.Key Changes
1. Declarative API & Metadata
@PartAnnotation: Defines the annotation interface withvalue,headers, andexplodebehaviors.PartMetadata&PartData: Distinguishes compile-time contract definition metadata (PartMetadata) from runtime invocation state (PartData).MultipartFormData: Holds the aggregated collection of runtime parts alongside standard invocation variables, ready to be passed downstream to the encoding layer.2. Contract Integration & Validation
DefaultContractParsing: Inspects parameters for@Part. If a single shorthand value lacking a colon (:) is provided, it automatically expands it into a standardContent-Disposition: form-data; name="..."structure.IllegalStateExceptionif a contract attempts to mix an unannotated body parameter with@Partparameters, or if a user provides bothvalueandheadersattributes simultaneously).3. Execution Factory Pathway
RequestTemplateFactoryResolver: Routes routing logic through a newBuildMultipartTemplateFromArgsfactory whenever@Partstructures are registered on a method contract.4. Encoder Implementation
MultipartFormEncoder: A newEncoderthat consumesMultipartFormDataorMultipartFormBodyobjects and serializes them into a standards-compliantmultipart/form-datarequest body with a generated boundary. Delegates to a configurable chain ofEncoderinstances for individual part body encoding.MultipartFormBody: A streamingRequest.Bodyimplementation that writes a complete multipart message (boundary-delimited headers + body for each part, followed by the closing boundary).PartBody: ARequest.Bodyrepresenting a single multipart part — headers and an optional body payload.PartBodyFactory: Iterates a configurable chain ofEncoderinstances to encode eachPartDatainto aPartBody, collecting suppressed exceptions for diagnostics.ContentDisposition: Parses and generatesContent-Dispositionheaders with properfilenameandfilename*(RFC 5987) encoding.Rfc5987Util: Implements RFC 5987 parameter value encoding for non-ASCII filenames inContent-Dispositionheaders.5. Spring Integration
MultipartFileEncoder: ExtendsMultipartFormEncoderto directly accept Spring'sMultipartFileobjects, automatically generating part headers (includingContent-Dispositionwith filename) and streaming the file content throughMultipartFileBody.PartHeadersFactory: Creates multipart part headers fromMultipartFilemetadata.Safety & Validation Rules Matrix
@Part("fieldName")Content-Disposition: form-data; name="fieldName"@Partand unannotated body paramIllegalStateException(Body conflict)valueandheaderson@PartIllegalStateExceptionvalueandheaderson@PartIllegalStateExceptionTest Coverage
DefaultContractMultipartTest: Thoroughly asserts metadata generation correctness, shorthand header translation, error-state rejection, explicit generic type resolution, and multiple part handling.BuildMultipartTemplateFromArgsTest: Confirms runtime template resolution factory behavior, ensuring variables map reliably and standard runtime execution faults transfer elegantly into FeignEncodeExceptiontypes.StreamingMultipartFormTest: End-to-end integration test covering the full pipeline — from@Part-annotated interface throughMultipartFormEncoder— including file uploads, byte arrays, strings, exploded collections, custom headers, non-ASCII filenames (RFC 5987), and empty/null part values.ContentDispositionTest: VerifiesContent-Dispositionheader parsing and generation, including filename encoding edge cases.Rfc5987UtilTest: Validates RFC 5987-compliant percent-encoding of parameter values.MultipartFileEncoderTest: Confirms SpringMultipartFileencoding viaMultipartFileEncoder, including single files, collections, and fallback to the delegate encoder for non-MultipartFiletypes.