-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for encoders even when no or multiple body parameters are present #1448
Comments
By the way, regarding how to implement this, I was thinking about providing a subclass of |
@fabiocarvalho777 Looks like we work together. Look me up at work and we can chat. |
HI Fabio, 0 bodies should be fine. Let's see if I got it correctly. By ZERO body you mean a client like this:
Just for checking sake, multi-body like this:
Multiple bodies, can be trick for 2 main reasons: Does my initial pointers make sense? |
Hello Marvin, Your ZERO body example is right, that is what we mean. The problem with it is that with a method like the one in the example, Feign skips any custom encoder set in the client (Feign will use And yes, your multi-body example is also correct.
That is right, any arguments that feign can't fit into something else, is set as body. However, that is only true for one single parameter. If the method has more than one parameter, then this exception is thrown (that is the roadblock we were hitting when we tried that approach).
That is actually an awesome idea! I had not thought about that! |
The only problem with using the map for the body object(s) is that the parameter name is not always necessarily known. And knowing the name of the parameter is important. That is why we were taking a different approach, providing a custom annotation for the parameters (containing the parameter name), registering them in Feign (so they wouldn't be counted as body), and then using our custom encoder to figure out how to write the body based on those annotated parameters. We need the encoder to be triggered though even if the method has zero body parameters. |
The map for the body object would only work if the key of the map is an integer (the order of the parameter), so, knowing that, we can get from another map the annotations of each parameter (using So, I believe there are two possible approaches:
The first approach is your idea, but using integer as key. I think we prefer approach number 2 because the name of the parameter is crucial for us (they would be used to set the GraphQL variable name) but, GraphQL and Java have different naming rules and restrictions, so Java parameter names sometimes would have to be different than what the end user wants for the GraphQL variable. What are your thoughts on this? Any objections on approach 2? I think approach 2 would be simpler as well in terms of backwards compatibility. All we would have to do is provide a new type of Feign contract, and only clients registered with that contract could use encoders that can be triggered with a zero body method. Just an idea. Or we can modify Feign |
Here is an example of what we are trying to achieve: @Query
List<Book> getBooks(@Variable("authorId") int authorId, @Variable("offset") int offset, @Variable("limit") int limit); In this example |
Yeah, we have that I think is a fair assumption to use arg1, arg2, arg3 if you don't include the compiler arg and don't include the annotation |
Ow, I see, ok, that seems to be a simple change to call the encoders with null. But, there is a big risk of breaking existing encoders left and right...
How does that sound?! |
That is why I suggested that we could create a new type of The I believe offering a new What do you think? By the way, I have been working a prototype of Thanks Marvin. |
@fabiocarvalho777 and I spoke and worked through the use case. The solution we landed on was to create an extension of the Proposed Solution/* name TBD */
interface RequestTemplateEncoder extends Encoder {
default void encode(Object body, Type bodyType, RequestTemplate template) {
/* delegate */
this.encode(template);
}
/**
* Encoder that derives the body from the Request Template
*/
void encode(RequestTemplate template);
} To support this, we'd need to make adjustments to @Override
protected RequestTemplate resolve(Object[] argv,
RequestTemplate mutable,
Map<String, Object> variables) {
Object body = argv[metadata.bodyIndex()];
try {
encoder.encode(body, metadata.bodyType(), mutable);
} catch (EncodeException e) {
throw e;
} catch (RuntimeException e) {
throw new EncodeException(e.getMessage(), e);
}
return super.resolve(argv, mutable, variables);
} ConsiderationsRemoving the /* update the interface method to indicate that object and type may be null */
void encode(@Nullable Object object, @Nullable Type bodyType, RequestTemplate template) throws EncodeException; AlternativeAn alternative is to do an implementation type check on the @Override
protected RequestTemplate resolve(Object[] argv,
RequestTemplate mutable,
Map<String, Object> variables) {
try {
if (RequestTemplateEncoder.class.isAssignableFrom(this.encoder.class) {
RequestTemplateEncoder enc = (RequestTemplateEncoder) this.encoder;
enc.encode(mutable);
} else {
Object body = argv[metadata.bodyIndex()];
checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
encoder.encode(body, metadata.bodyType(), mutable);
}
} catch (EncodeException e) {
throw e;
} catch (RuntimeException e) {
throw new EncodeException(e.getMessage(), e);
}
return super.resolve(argv, mutable, variables);
} While this change maintains the existing RecommendationMy recommendation is to go with the second solution, and update the Thoughts? |
I think just having nullable arguments on
But, by doing so, we need to make a major release, as that has major impacts on any Encoders out there that assume the Or, we could expand on the implementation type check idea. We leave When a But, I don't think |
Then maybe we should use the alternative where we do an interface check. That way we can release it now and deprecate/document the changes for the next major release. Will that work? |
Yes, I just updated my comment with that in mind. |
instead of doing:
we would do:
The else block is only to throw the exceptions as it used to. |
Hello, Thanks @velo and @kdavisk6 for taking the time to look at this!! We appreciate it! There is just one other detail though that needs to be addressed (Kevin, I hadn't noticed it by the time we had our meeting). Correct if I am wrong, but With that additional change, the new encode method would look like this: void encode(Object[] arguments, RequestTemplate template); What are your thoughts about this? Is it ok to add |
The template should be resolved by then so you can access the values using the query and header methods on Request Template. I’ll verify. If not, it’s simple to change the order so it is resolved. |
That would totally break |
@velo Only if we are modifying the original encode method. But that is not the idea here, and is not what Kevin documented here a few hours ago. The idea is to add a second encode method. That is the one I am referring to. See Kevins Proposed solution above (his first comment today). |
I see. I believe there are a few issues (see below) with relying on
|
hrmmm, I suggest preserving the original signature, but using That way, the same code runs when body is present or when it's missing, but the Multiple Also, we could just use an empty map and avoid the issue of nulls... but still, that would change behaviour for all existing Encoders out here. |
@fabiocarvalho777 when you propose so, if I have |
@fabiocarvalho777 If I recall you mentioned you were using a custom Contract. I assumed that you could manage mapping your annotations and the resulting Method Metadata would have the method arguments mapped to query parameters. I’m sorry if I misunderstood, but that would be the best way forward fo you here. I’ll write up an example that explains how to achieve what you are looking for with the approach outlined above. I’m certain we can find a solution that works without breaking the current assumptions around Encoder. |
If a client is using those Feign annotations ( The proposed solution Kevin posted here (the one with an extension of |
Correct. That was before your suggestion to have a new
|
Yes but create two code paths... and then it raises the question of what should happen if both code paths could be executed. Regardless of what Contract is been used, I'm curious to understand what the Let's say I'm using my own home brew Contract. Arg1 is a path param, Arg2 is a header, Arg3 and Arg4 are bodies. What would the object[] look like? |
The contract for the new Encoder interface should make it clear that the Feign annotations are ignored when that second encode method is called. The user should know then, when he or she explicitly chooses to use that Encoder interface, he or she will gain access to a different integration experience. As long as the contract is clear, and documented in the javadoc, it shouldn't be a problem. Also, please correct me if I am wrong, but isn't that similar to the situation today when defining the body based on |
So, effectively, this should be an Encoder that disables/bypasses Contract?! |
No, the contract is not being bypassed. We use a Contract to register all our custom annotations. |
But still, the new Encoder, would have access to all data contract has access to in order to create a body?! |
Correct. |
I'm wondering if we shouldn't fix this on the |
Yes, actually that is what I thought too at first. Kevin then recommended to use the In fact, I already finished a prototype for the Encoder approach and it worked. You can see it here. If you want, I can try a prototype solving the problem in the |
Now that I understand the problem a bit better, seems @kdavisk6 I guess is just a matter of understanding why you prefer |
I am in the process of developing a prototype based on a new However, just to save time, @kdavisk6, is that second prototype worth it? Or do you still think the I don't have a preference at this point. The |
I have just finished the second prototype (based on modifying I tested them both and they both work as expected. Obviously anything can be changed in both options, just let me know and I will do it. So, which of the two options do you prefer? Thanks. |
The contract part was something we spoke about in our discussion, but I felt it would be something that could end up being another Let's go with adding a new parameter, something like Should we see about adding to a method level or is the |
Currently Feign will not resolve methods with
BuildEncodedTemplateFromArgs
if they have 0 or more than 1 body parameters, which prevents the usage of custom Feign encoders. There are use cases though where an application (or library) needs a client with a custom encoder and needs that encoder triggered even if the client method has 0 or more than 1 "body parameters" (parameters not annotated with Feign annotations or custom annotations registered in Feign usingDeclarativeContract.registerParameterAnnotation
).A few examples of this use case:
DeclarativeContract.registerParameterAnnotation
. In this case the encoder will use all the parameters, plus metadata in their respective annotations, to properly define the request message payload.To be more specific, this issue is required to support certain use cases in Mocca. This improvement would help to simplify Mocca's API a lot.
We are also open to contributing a PR to implement this feature, if Feign team first accesses this issue and let us know we should do so.
By the way, we tried first to address this use case by creating a custom Feign Contract implementation, but that was not possible because overwriting a
parseAndValidateMetadata
requires instantiatingMethodMetadata
and that class has a package-private constructor (besides being final too).Please let us know your thoughts on this and, if you believe this is an useful enhancement to have, if we can go ahead and provide a PR addressing this (or if you prefer to do so yourselves, please let us know the timeline to provide it).
Thanks.
The text was updated successfully, but these errors were encountered: