-
Notifications
You must be signed in to change notification settings - Fork 38.4k
ETag/If-None-Match logic in HttpEntityMethodProcessor should not affect methods other than HTTP GET [SPR-13496] #18074
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
Comments
Brian Clozel commented Thanks for reporting this! |
Francisco Lozano commented Thanks for the fix ... anyway, it'd be great if all this magic etag handling was fully optional somehow. |
Brian Clozel commented Francisco Lozano It's always hard to draw the line between "giving options, not enforcing strong opinions" and "being easy to configure". If you think this conditional requests support can be problematic, you should definitely open a new enhancement issue for you and other developers to describe their use cases and vote for this request. |
Francisco Lozano commented well it's a quite drastic change that may pass unnoticed in many web apps, but for sure affects any REST API implementation that is already using etags and handling these internally. If you add a new behaviour that silently breaks stuff, I think a way to revert that should be provided... and that's not an "enhancement", that's just "allow to optionally not-break existing stuff". |
Rossen Stoyanchev commented After the fix for this issue to constrain the behavior to HTTP GET, is there anything else that still breaks for you? Could you elaborate? Thanks. |
Francisco Lozano commented The remaining issue is that older REST clients that don't handle caching correctly may send If-Modified-Since / If-None-Match when doing GET but still not support conditional request/response flow correctly. I have clients with broken cache/conditional behaviour like that, unfortunately. Those clients are working well without this feature, but they will break immediately if I release my server with Spring 4.2. Browsers are open and flexible, but poorly written HTTP clients are not and simple changes like these can break them. I understand the new behaviour is RFC-compliant, but it can break existing apps - it would break my app. I think an opt-out for it is necessary. |
Brian Clozel commented I'm confused - are you using Not using those methods should be the simplest way to opt out; if REST clients have a poor conditional request implementation, then those response headers are useless and could confuse them. |
Francisco Lozano commented Yes, I am using those headers, especially eTag, I use heavily for conditional updates and such - that's why I got affected by this issue in the first place... I use by directly writing headers, not using the specific methods in ResponseEntity. I don't think when I started writing my code these existed (not sure now). I cannot not-use those headers as they're part of my API. However, I have clients that are currently working (since Spring 3.0), that have fragile conditional request implementation and that will fail if the server ever returns 304. That's the exact part that I need to opt-out, the GET returning 304 and skipping the body: I have clients that I don't control and that will break with this behaviour. My point is that this change enforces me to change my API's contract with no possibility to opt-out, and I don't think that's fair at all. |
Brian Clozel commented We could provide an option to disable this: you'd have to configure your own This would probably make your application configuration more complex, as I believe this would prevent the use of Thoughts? |
Francisco Lozano commented That would be painfully complex configuration, right? mostly code copy&paste... |
Francisco Lozano commented Is this going to stay like this definitely? My API is broken with 4.2 and I cannot do anything about it other than copy&paste a lot of Spring internals and modify myself. |
Juergen Hoeller commented Brian Clozel, Rossen Stoyanchev, I suppose we should create a follow-up JIRA issue that's specifically about a convenient opt-out variant? |
Brian Clozel commented My previous comment described a possible workaround for this, but it seems that this solution is not acceptable for you. Where would you see this new configuration? Could you elaborate on those HTTP clients, as knowing their name/popularity could tell us how big is the audience for this? |
Francisco Lozano commented
The workaround described, as I answered, involves copying Spring internal code that I, as a framework user, should have no business touching (let alone copying into my classes). My configuration code would brutally explode just for disabling a new feature. I think one possibility would be to allow to configure (disable) the behaviour in the WebMvcConfigurationSupport class - like the configuration methods that allow you to customize view resolvers or cors mappings from there.
When I say client I mean a client application (mobile app, iot device) with an existing software running and a set of expectations, not a specific http client library. I can show you an example of code that uses RestTemplate that becomes broken with this change (extracted from integration test): // Headers
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaderConst.AUTHORIZATION, "Bearer " + token;
headers.setIfNoneMatch("*");
// Entity
final HttpEntity<?> requestEntity = new HttpEntity<>(headers);
// Get object body via REST
final ResponseEntity<InputStream> resObjBody = restTemplate.exchange(
url, HttpMethod.GET, requestEntity, InputStream.class);
byte[] obtained = IOUtils.toByteArray(resObjBody.getBody()); With 4.2 in the server, a NPE will happen in the last line. Any app client that has above expectation will fail, no matter what client technology is used. I could force clients to update if I controlled them (that would be bad), but I don't control them and thus I cannot force anything (I can just break my API). You could say above code is "wrong" as per RFC (that's debatable), but it's a compatibility break with no possible opt-out: It worked with a server based in 4.1, and cannot work with a server based in 4.2 because the new code enforces the server to answer with 3xx and the response body is null. It could be acceptable (again, debatable) in a pure Web/browser environment, but definitely it's not in an environment in which clients can be applications or devices. |
Brian Clozel commented Overloading an existing MVC Bean is a well described solution (see Javadoc) for specific needs that are rare or behavior that we tend to warn against.
Since we're talking specific applications, not libraries, this makes even a stronger case for a configuration flag that will be rarely used.
This behavior shows an HTTP client asking for potential HTTP 304 and breaking on empty bodies. I don't think this new feature qualifies as a compatibility break; this is in fact a very small feature dealing with a very old, battle-tested, ubiquitous spec: HTTP. HTTP clients (not only browsers) sending conditional requests should know how to deal with them. Now I totally understand (and sympathize) the fact that you can't change those clients and that you have to deal with a production application.
|
Francisco Lozano commented I guess it's not my call and you are not changing your mind about this, but I would not consider conditional request handling a "small MVC new feature" - it's maybe small with regard to implementation, but the surface impact it has is enormous. Why CORS deserves configuration in WebMvcConfigurationSupport and conditional request handling does not? About the possible solutions:
It would be OK to have my own HttpEntityMethodProcessor, but having my own RequestMappingHandlerAdapter and reimplement getDefaultArgumentResolvers() goes beyond reasonable "customization", as that method really does a lot of very heterogeneous stuff. Moreover, that method is private and uses private state (requestResponseBodyAdvice) that I could not touch. I could override afterPropertiesSet and call super() methods to initialize the original state and then initialize a manipulated version... but I guess you can see how complex and unreliable this could be. If you could make it a little easier to customize RequestMappingHandlerAdapter, it could be a good way to solve it, but in current state I don't think that it can be considered practical to customize RequestMappingHandlerAdapter beyond the few setters that there are.
|
Brian Clozel commented I'm always open to new arguments and happy to change my mind. Right now, I don't think this use case qualifies for a configuration facility at the I agree that #2 is quite involved and harder to maintain, but that's the best solution I can come up with right now on our side. I still believe that a Filter/Interceptor solution is the best choice, but that's not my call. Again, involving the community is a way forward - gathering more use cases and traction behind this would only help. |
Francisco Lozano commented Thanks for your comments and explanation. I don't think this is an acceptable answer, but I am giving up fighting it. |
Juergen Hoeller commented Francisco Lozano, please don't give up. We want you to be able to upgrade to 4.2+ without too much concern... In particular, opening up Juergen |
Rossen Stoyanchev commented There have been several ideas mentioned. We are simply trying to find some common ground. So indeed don't give up, your feedback is valuable. A slightly different direction but looking at the example above with GET +
Isn't this always a 304? The "*" is clearly not intended for GET. Perhaps you omitted some other headers for brevity? If this is the exact situation you have then there might be a simple solution. For once Brian's suggestion to wrap the request to suppress the "If-None-Match" can be an effective workaround. We can also address this in HttpEntityMethodProcessor itself. If we see a GET with Even the spec has clarifications for undefined combinations of headers, for example:
So if we encounter something we can't make sense of, we simply ignore it and effectively treat it as invalid (the client doesn't know what they're doing). So could it be as simple as that? |
Rossen Stoyanchev commented I've updated the title to reflect the actual fix for this ticket in 4.2.2 (something we can no longer change). I've also created a separate ticket #18204 to discuss the remaining questions. |
Francisco Lozano opened SPR-13496 and commented
A new etag-related logic has been introduced that breaks backward compatibility.
In HttpEntityMethodProcessor, the line:
maybe acceptable for GET, but definitely not for PUT/POST/PATCH verbs, because if I do:
the intention is to create the resource only if it doesn't exist previously. Forcing 304 not modified makes absolutely no sense here, because I want to return 201 created (which I set in my ResponseEntity<MyResponseObject>).
I haven't find any obvious way to disable this magic etag handling, that's why I'm marking this issue as blocker (feel free to downgrade if a workaround is available).
I think all this logic may make sense in some applications, but it can introduce heavy incompatibilities in existing REST APIs.
I wish I had found this when in RC - but I couldn't because of other issues of my components with 4.2 that only now I have been able to fix.
Full changes on this class:
Affects: 4.2 GA, 4.2.1
Issue Links:
Referenced from: commits 583a48a
1 votes, 5 watchers
The text was updated successfully, but these errors were encountered: