3

I'm trying to migrate from Spring Boot 1.5.7 to 2.0.0.M4

This is my rest controller:

@RestController
@RequestMapping("/v1.0/users")
public class UsersController {

    @Autowired
    private UserService userService;


    @RequestMapping(value = "/validate-username/{username}", method = RequestMethod.GET)
    @ResponseStatus(value = HttpStatus.OK)
    public void validateUsername(@PathVariable String username) {
        throw new EntityAlreadyExistsException();
    }

...

}

This is exception handler:

@ControllerAdvice
public class GlobalControllerExceptionHandler {

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.CONFLICT)
    public Map<String, ResponseError> handleEntityAlreadyExistsException(EntityAlreadyExistsException e, HttpServletRequest request, HttpServletResponse response) throws IOException {
        logger.debug("API error", e);
        return createResponseError(HttpStatus.CONFLICT.value(), e.getMessage());
    }

}

in case of the following username, for example : alex everything is working fine and I'm receiving 409 status code with application/json; charset=UTF-8 as a content type but in case of the following user name, for example, alex@test.com my endpoint returns 500 status code and non-JSON content type, something like this:

enter image description here

I can reproduce this issue when username PathVariable contains .com at the end.

I use embedded Tomcat as tbe application server. Woth Spring Boot 1.5.7 the same functionality was working fine. How to make it working with Spring Boot 2.0.0.M4 ?

P.S.

I know that sending email addresses as URL parameter is a bad practice. I'm just interested in this particular case.

alexanoid
  • 24,051
  • 54
  • 210
  • 410

4 Answers4

2

Try this buddy, {username:.+} instead of your {username}

EDIT: I've found out that @ is reserved character for URL, and should not be used in URL. You need to encode your URL.

Something like: alex@test.com - > alex%40test.com

source : Another stackoverflow question

Amiko
  • 545
  • 1
  • 8
  • 26
  • Did you try inputting `produces = MediaType.APPLICATION_JSON_VALUE` ? – Amiko Oct 06 '17 at 13:14
  • This way it produces following error - `TTP Status 406 – Not Acceptable Type Status Report Description The target resource does not have a current representation that would be acceptable to the user agent, according to the proactive negotiation header fields received in the request, and the server is unwilling to supply a default representation.` – alexanoid Oct 06 '17 at 13:20
  • Try my first suggestion with @Pathvariable(name="username"), doesn't hurt :P. Also checkout my small research – Amiko Oct 06 '17 at 13:23
  • Unfortunately still doesn't work even with URLEncoder.encode(EMAIL, "UTF-8") – alexanoid Oct 06 '17 at 14:12
  • No, I think you need to send already encoded email to that RestController and then decode it. – Amiko Oct 06 '17 at 15:29
  • Yeah, I mean the same. I used URLEncoder.encode() at the Java client for my API – alexanoid Oct 06 '17 at 17:22
2

This article shows how one can stop Spring from using path extensions for content negotiation (worked for me for Spring Boot 1.5.x at least):

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override 
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { 
        configurer.favorPathExtension(false); 
    }
}
Glen Mazza
  • 748
  • 1
  • 6
  • 27
1

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

geoand
  • 60,071
  • 24
  • 172
  • 190
1

A shortest possible solution is to modify your URL like below

@RequestMapping(value = "/validate-username/{username}/"

Note: I use slash '/' at the end of the URL. So, the URL will work perfectly for any kind of email address or .com , .us containing text in your {username} path variable. You don't need to add any kind of extra configuration to your application.

abestrad
  • 898
  • 2
  • 12
  • 23