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 |
|
@yvasyliev apologies for the delayed response - was traveling. I'm digging into your question about the PartDataFlattener, etc... utility classes being in core... This is a fair question and important for us to consider. The question is this: Are we willing to do something very ugly with the architecture to preserve the module separation for multi-part handling? Because trying to do everything in the encoding layer is going to do some very ugly things. I think that once we have decided that multi-part has to be handled in core, then there is very little reason to package some pieces of it out into separate modules. The ideal, of course, would be if Looking at the utility classes you asked about, there are two categories: Category 1 - Category 2 - If we want to keep the core api file hierarchy clean, what do you think about having a dedicated MultipartRequestTemplateFactory.java file, then put everything related inside that file as private static classes? |
|
I have a few thoughts on the latest MultipartFormEncoder... First off, I want to make clear that the approach does work. I just think that it requires users to do some very awkward configuration (I'll give concrete example of this down below). I have a couple of comments specific to the approach itself (the code as written is great - all of my comments are about the approach itself):
With this latest approach, the user has to think about all sorts of things to enable multipart handling (including combining encoders together to create a PartBodyFactory). So let's assume that I want to use the behavior of I'll point out that it isn't even possible to easily set up the encoder chain that I described here with current Feign classes, so let's assume that I also have created a MultiEncoder class that loops a list of encoders until it finds one that doesn't throw (assuming that is even a reliable way of determining whether a given encoder actually encoded or not): So yes, it is doable. But it's nowhere near as clean as the following (which is what would be possible if we do the work in a MultipartRequestTemplateFactory class): I know that I would prefer to write the second example :-) The one downside is that if the user truly wants completely different behavior for the multipart handling than for other requests (e.g. if they want to use a different encoder based on a content-type predicate for multipart vs other Feign methods), they will have to create a separate Feign config for multipart. But I think this scenario is very, very unusual. I'm looking forward to hearing your responses! |
|
I haven't had much free time lately either. 😊 Are you willing to get your hands dirty and submit a change by yourself? Regarding your comments:
The utility classes were originally private methods in my code. To make the code clean, I moved the private methods to package-private utility classes. I find the utility classes ugly by themselves regardless of whether we put them in the If we forget about Feign and this PR for a moment, The way I see it, the Additionally, there's a logical problem with the suggested approach: Thus, the reason I'd prefer to keep the utility classes in the
We have to assume it unless we want to introduce breaking changes in the
The scope of this change is multipart form encoding only. I see your arguments for the MultipartFormEncoder.builder()
.partBodyFactory(
new PartBodyFactory(
List.of(
new Encoder1(),
new Encoder2(),
new Encoder3() )))
.build()becomes MultipartFormEncoder.builder()
.partBodyEncoders(
List.of(
new Encoder1(),
new Encoder2(),
new Encoder3() ))
.build()For Hey @velo, we don't seem to see eye to eye on the multipart form encoding architecture. Do you have any thoughts/recommendations? I'm happy to proceed with whichever approach has stronger consensus. |
|
@yvasyliev yes - I can definitely get my hands dirty :-). I have time Friday that I can focus on this.
Yes, I definitely remember it! My proposed "solution" for this is to have encode() return a boolean indicating success or failure. But if we don't want to do that, then I guess an even easier fix will be to change the javadoc to indicate "If this encoder is unable to encode the result, it must throw an EncodeException. Successful return from this method indicates that encoding was performed."
I respect that - however, I think that it is important to think about the big picture and not duplicate code. If we see a need (and I truly do) to handle a chain-of-responsibility for encoders, then it would be unfortunate to solve that problem locally (specific to this PR), then find that our local solution isn't compatible with some future global solution. There are some fundamental issues with how Feign handles encoder registration, and if fixing those is surgical, non-breaking, and creates an elegant solution for the current PR, then I think we should seriously consider it. |
|
I think that it is helpful to lay out real-world examples where Multipart handling is a part of a bigger Feign usage. This is where the awkwardness of the current approach becomes visible. My opinion is that architecture is super important, but not if it gets in the way of the API being intuitive. To make this clear, I started thinking about what kind of migration tutorial documentation we would need to create to encourage people to move to the new approach. Let's look at the user experience using the existing multipart handling: now consider how this would have to change for the current PR approach (PS - I'm replacing the List.of() call with vararg method to clean things up a little bit further): Yes - it works, but now the user has to think about partbody encoders vs regular encoders, they need to understand delegates - and almost certainly will wind up having to duplicate creation of encoders. For a complex scenario with multiple encoders, it becomes awkward. But - and this is really important: the above example doesn't even support streaming upload of File that aren't multipart. To support direct File upload streaming, we have to somehow get an encoder that can handle multiple types - so something like DefaultEncoder - but now we need to add FileEncoder to that. Yes - we could always add more things to DefaultEncoder - but that is really getting away from the Feign design intent. With the approach I am proposing, this becomes as follows (this assumes we make encoder() accept vararg in the Feign config builder): The user doesn't have to understand anything about MultipartFormEncoder other than the fact that they want to support multipart, file, and JSON encoded endpoints. It becomes much more like a plugin based architecture, and I think that is desirable. |
Sorry - I missed this question. The answer is because the user may want different encoding strategies for MultipartForm. We could, for example, have another encoder implementation that does URL query parameter encoding. I just don't see any reason to take the decision away from the Feign system-wide encoder(s). The general approach I'm proposing is exactly what we would do if we manually constructed a MultiPartForm without the annotations:
|
|
@yvasyliev I may have found a way to get the best of both worlds... What if we added the encoder to the MultipartFormData constructor and pass it in from BuildMultipartTemplateFromArgs? Then the MultpartFormEncoder can construct a PartBodyFactory on the fly (or pass encoder to the PartBodyFactory.create() method) as it encodes each part. If we do this, then the multipart encoder will use the Feign-wide encoder, but still keep all those utility classes in the form module. |
|
Hey @trumpetinc, I've spent time re-reading our discussion and researching how other libraries (Spring HTTP Interface,
What concerns me is that introducing Bottom line: I think we should tackle this in two steps — first a focused PR to add Just an FYI: I'm itching to start coding this. Let me know if you want to take the lead on the implementation, otherwise PS: the |
|
@yvasyliev please see my proposal for allowing I think if we do this that we get the best of both worlds... |
|
As for fixing the Encoder chaining, my proposal is that instead of adding another method to Encoder, we change the return type of encode() to be a boolean,. If that returns true, then encoding was handled. If it returns false, then the encoder did not support the encoding request. If an EncodeException is thrown, it means that the encoder could have handled the request, but something bad happened that the user needs to know about. I think there are a lot of advantages to doing it this with vs adding another method:
The reality is that we are already making a breaking change with 14.X, so I think this is a pretty minor change to users. I do acknowledge that we'll need to rewrite all of the existing encoders so they return false instead of throwing, but this seems like a pretty reasonable change. |
I completely support this - it is much cleaner. |
I agree with the two PRs as you describe. It will be VERY nice to fix the encoder chaining issue! I do want to get the canEncode() vs boolean encode() decision nailed down. ** EDIT: ** I have changed my mind. I think that using a separate method with default implementation is a problem. We don't want to have the multi-encoder catch EncodeException at all. If any encoder throws an EncodeException, it should halt the encoding sequence so the user will know what the problem is. If we use the default method approach, then I think the multi-extractor is going to have to still catch EncodeException (to handle the legacy cases), which undermines the entire purpose of having the other method! I would say that you should proceed with the PR you proposed (I won't be able to work on any of this until tomorrow). I am at my desk right now, so I'll see any responses and reply quickly (and you have my direct email address if you want to move quicker). |
So, would it be a problem to do something like this? @RequiredArgsConstructor
class DelegatingEncoder implements Encoder {
@NotNull private final List<Encoder> delegates;
@Override
public void encode(
Object object,
Type bodyType,
RequestTemplate template
) throws EncodeException {
for (Encoder delegate : delegates) {
if (delegate.canEncode(object, bodyType, template)) {
delegate.encode(object, bodyType, template);
return;
}
}
throw new EncodeException("Not encoded");
}
@Override
public boolean canEncode(
Object object,
Type bodyType,
RequestTemplate template
) {
return delegates.stream()
.anyMatch(delegate -> delegate.canEncode(
object,
bodyType,
template
));
}
}It's late evening for me. I won't start implementing anything till tomorrow either 😅 |
|
@yvasyliev The problem is going to be caused by the default method implementation in Encoder: The result of this is that every "old" Encoder is going to get it's encode() method called, which will then throw EncodeException. So the first old Encoder that you hit that can't handle the request will cause the entire encode operation to fail, even though there are probably other encoders later in the chain that can handle the request. And that failure is a runtime failure which will be nasty to detect (a user could have some Feign endpoints that work fine, then others that would fail, all depending on the order of the encoders in the chain). I really think it is much better to force a compile-time error. |
|
@yvasyliev I really think we are close to having this completely done! Because you will be awake earlier than I will, please go ahead and run with the two PRs - you have earned a huge amount of appreciation from the community for all your work on this. Summarizing the open discussion items (I think this is all that is left):
PS: What do you think about changing the Feign config |
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.