The issue you are observing goes deep into the Spring WebMvc internals.
The root cause is that Spring is speculating about the accepted response type.
In detail, the strategy class that is actually providing the answer for the accepted response type in case of alex@test.com
is ServletPathExtensionContentNegotiationStrategy, which makes a guess based on what is finds in the path.
Due to the fact that com
is a valid file extension type (see this), Spring Boot 2.0.0.M4 tries to use that mime type to convert your response from your ControllerAdvice
class to that mime type (and of course fails) and therefore falling back to it's default erroneous response.
A first though to getting around this issue would be to you specify the HTTP header Accept
with a value
of application/json
.
Unfortunately Spring 2.0.0.M4 will still not use this mime type because the ServletPathExtensionContentNegotiationStrategy strategy takes precedence over HeaderContentNegotiationStrategy.
Moreover alex
is used or (even something like alex@test.gr
for that matter), no mime type is being guessed by Spring, therefore allowing for the regular flow to proceed.
The reason that this works is Spring Boot 1.5.7.RELEASE is that Spring is not attempting to map com to a mime type, and therefore a default response type is used which allows the process of converting the response object to JSON to continue.
The difference between the two version boils down to this and this.
Now comes the even more interesting part, which is of course the fix.
I have two solutions in mind, but I will only show the first one and just mention the second.
Here is the first solution which build upon my explanation of the problem.
I admit that this solution does seem a bit intrusive, but it works a like a charm.
What we need to do is alter the auto-configured ContentNegotiationManager
in order to replace the supplied PathExtensionContentNegotiationStrategy
with our own custom one. Such an operation can easily be performed by a BeanPostProcessor
.
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.context.request.NativeWebRequest;
import java.util.ListIterator;
@Configuration
public class ContentNegotiationManagerConfiguration {
@Bean
public ContentNegotiationManagerBeanPostProcessor contentNegotiationManagerBeanPostProcessor() {
return new ContentNegotiationManagerBeanPostProcessor();
}
private static class ContentNegotiationManagerBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean; //no op
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (!(bean instanceof ContentNegotiationManager)) {
return bean;
}
final ContentNegotiationManager contentNegotiationManager = (ContentNegotiationManager) bean;
ListIterator<ContentNegotiationStrategy> iterator =
contentNegotiationManager.getStrategies().listIterator();
while (iterator.hasNext()) {
ContentNegotiationStrategy strategy = iterator.next();
if (strategy.getClass().getName().contains("OptionalPathExtensionContentNegotiationStrategy")) {
iterator.set(new RemoveHandleNoMatchContentNegotiationStrategy());
}
}
return bean;
}
}
private static class RemoveHandleNoMatchContentNegotiationStrategy
extends PathExtensionContentNegotiationStrategy {
/**
* Don't lookup file extensions to match mime-type
* Effectively reverts to Spring Boot 1.5.7 behavior
*/
@Override
protected MediaType handleNoMatch(NativeWebRequest request, String key) {
return null;
}
}
}
The second solution one could implement is leverage the capability of OptionalPathExtensionContentNegotiationStrategy class which is used by Spring by default.
Essentially what you would need to do is ensure that every HTTP request to your validateUsername
endpoint would contain an attribute named org.springframework.web.accept.PathExtensionContentNegotiationStrategy.SKIP
with the value of true