Question

I have a REST controller:

@RequestMapping(value = "greeting", method = RequestMethod.GET, produces = "application/json; charset=utf-8")
@Transactional(readOnly = true)
@ResponseBody
public HttpEntity<GreetingResource> greetingResource(@RequestParam(value = "message", required = false, defaultValue = "World") String message) {
    GreetingResource greetingResource = new GreetingResource(String.format(TEMPLATE, message));
    greetingResource.add(linkTo(methodOn(AdminController.class).greetingResource(message)).withSelfRel());
    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.add("Content-Type", "application/json; charset=utf-8");
    return new ResponseEntity<GreetingResource>(greetingResource, responseHeaders, HttpStatus.OK);
}

As you can see, I'm trying hard to specify the content type returned by the controller.

It is accessed with a REST client:

public String getGreetingMessage() {
    String message;
    try {
        HttpHeaders httpHeaders = Common.createAuthenticationHeaders("stephane" + ":" + "mypassword");
        ResponseEntity<GreetingResource> responseEntity = restTemplate.getForEntity("/admin/greeting", GreetingResource.class, httpHeaders);
        GreetingResource greetingResource = responseEntity.getBody();
        message = greetingResource.getMessage();
    } catch (HttpMessageNotReadableException e) {
        message = "The GET request FAILED with the message being not readable: " + e.getMessage();
    } catch (HttpStatusCodeException e) {
        message = "The GET request FAILED with the HttpStatusCode: " + e.getStatusCode() + "|" + e.getStatusText();
    } catch (RuntimeException e) {
        message = "The GET request FAILED " + ExceptionUtils.getFullStackTrace(e);
    }
    return message;
}

The http headers are created by a utility:

static public HttpHeaders createAuthenticationHeaders(String usernamePassword) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
    byte[] encodedAuthorisation = Base64.encode(usernamePassword.getBytes());
    headers.add("Authorization", "Basic " + new String(encodedAuthorisation));
    return headers;
}

The web security configuration and code work fine. I make sure of this using a mockMvc based integration test which succeeds.

The only test that fails is the one based on the REST template:

@Test
public void testGreeting() throws Exception {
    mockServer.expect(requestTo("/admin/greeting")).andExpect(method(HttpMethod.GET)).andRespond(withStatus(HttpStatus.OK));
    String message = adminRestClient.getGreetingMessage();
    mockServer.verify();
    assertThat(message, allOf(containsString("Hello"), containsString("World")));
}

The exception given in the Maven build console output is:

java.lang.AssertionError: 
Expected: (a string containing "Hello" and a string containing "World")
got: "The GET request FAILED org.springframework.web.client.RestClientException : Could not extract response: no suitable HttpMessageConverter found for response type [class com.thalasoft.learnintouch.rest.resource.GreetingR esource] and content type [application/octet-stream]\n\tat org.springframework.web.client.HttpMessageConverte rExtractor.extractData(HttpMessageConverterExtract or.java:107)

I'm using the Spring Framework 3.2.2.RELEASE version and the Spring Security 3.1.4.RELEASE version on the Java 1.6 version.

At first, I had a bare bone REST template:

@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    return restTemplate;
}

I have now added to it, hoping it would help:

private static final Charset UTF8 = Charset.forName("UTF-8");

@Bean
public RestTemplate restTemplate() {
    List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
    MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();        
    mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "json", UTF8)));
    messageConverters.add(mappingJackson2HttpMessageConverter);

    Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
    jaxb2Marshaller.setClassesToBeBound(new Class[] {
        GreetingResource.class
    });
    MarshallingHttpMessageConverter marshallingHttpMessageConverter = new MarshallingHttpMessageConverter(jaxb2Marshaller, jaxb2Marshaller);
    messageConverters.add(marshallingHttpMessageConverter);

    messageConverters.add(new ByteArrayHttpMessageConverter());
    messageConverters.add(new FormHttpMessageConverter());
    StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
    stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("text", "plain", UTF8)));
    messageConverters.add(stringHttpMessageConverter);
    messageConverters.add(new BufferedImageHttpMessageConverter());
    messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
    messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setMessageConverters(messageConverters);
    return restTemplate;
}

But it didn't change anything and the exception remains the same.

My understanding is that, it is not the REST template that needs any specific JSON configuration, but rather, that, for some reason, my controller is spitting out some application/octet-stream content type instead of some application/json content type.

Any clue?

Some additional information...

The admin rest client bean in the web test configuration:

@Configuration
public class WebTestConfiguration {

    @Bean
    public AdminRestClient adminRestClient() {
        return new AdminRestClient();
    }

    @Bean
    public RestTemplate restTemplate() {
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
        MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();        
        mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "json", UTF8)));
        messageConverters.add(mappingJackson2HttpMessageConverter);

        Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
        jaxb2Marshaller.setClassesToBeBound(new Class[] {
            Greeting.class
        });
        MarshallingHttpMessageConverter marshallingHttpMessageConverter = new MarshallingHttpMessageConverter(jaxb2Marshaller, jaxb2Marshaller);
        messageConverters.add(marshallingHttpMessageConverter);

        messageConverters.add(new ByteArrayHttpMessageConverter());
        messageConverters.add(new FormHttpMessageConverter());
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
        stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("text", "plain", UTF8)));
        messageConverters.add(stringHttpMessageConverter);
        messageConverters.add(new BufferedImageHttpMessageConverter());
        messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
        messageConverters.add(new AllEncompassingFormHttpMessageConverter());
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setMessageConverters(messageConverters);
        return restTemplate;
    }

}

The base test class:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration( classes = { ApplicationConfiguration.class, WebSecurityConfig.class, WebConfiguration.class, WebTestConfiguration.class })
@Transactional
public abstract class AbstractControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Autowired  
    protected RestTemplate restTemplate;  

    protected MockRestServiceServer mockServer;

    @Before
    public void setup() {
        this.mockServer = MockRestServiceServer.createServer(restTemplate);
    }

}

The web init class:

public class WebInit implements WebApplicationInitializer {

    private static Logger logger = LoggerFactory.getLogger(WebInit.class);

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        registerListener(servletContext);

        registerDispatcherServlet(servletContext);

        registerJspServlet(servletContext);

        createSecurityFilter(servletContext);
    }

    private void registerListener(ServletContext servletContext) {
        // Create the root application context
        AnnotationConfigWebApplicationContext appContext = createContext(ApplicationConfiguration.class, WebSecurityConfig.class);

        // Set the application display name
        appContext.setDisplayName("LearnInTouch");

        // Create the Spring Container shared by all servlets and filters
        servletContext.addListener(new ContextLoaderListener(appContext));
    }

    private void registerDispatcherServlet(ServletContext servletContext) {
        AnnotationConfigWebApplicationContext webApplicationContext = createContext(WebConfiguration.class);

        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(webApplicationContext));
        dispatcher.setLoadOnStartup(1);

        Set<String> mappingConflicts = dispatcher.addMapping("/");

        if (!mappingConflicts.isEmpty()) {
          for (String mappingConflict : mappingConflicts) {
            logger.error("Mapping conflict: " + mappingConflict);
          }
          throw new IllegalStateException(
              "The servlet cannot be mapped to '/'");
        }
    }

    private void registerJspServlet(ServletContext servletContext) {
    }

    private AnnotationConfigWebApplicationContext createContext(final Class... modules) {
        AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
        appContext.register(modules);
        return appContext;
    }

    private void createSecurityFilter(ServletContext servletContext) {
        FilterRegistration.Dynamic springSecurityFilterChain = servletContext.addFilter("springSecurityFilterChain", DelegatingFilterProxy.class);
        springSecurityFilterChain.addMappingForUrlPatterns(null, false, "/*");
    }

}

The web configuration:

@Configuration
@EnableWebMvc
@EnableEntityLinks
@ComponentScan(basePackages = "com.thalasoft.learnintouch.rest.controller")
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        PageableArgumentResolver resolver = new PageableArgumentResolver();
        resolver.setFallbackPageable(new PageRequest(1, 10));
        resolvers.add(new ServletWebArgumentResolverAdapter(resolver));
        super.addArgumentResolvers(resolvers);
    }

}

The application configuration is empty for now:

@Configuration
@Import({ ApplicationContext.class })
public class ApplicationConfiguration extends WebMvcConfigurerAdapter {

    // Declare "application" scope beans here, that is, beans that are not only used by the web context

}
Was it helpful?

Solution

I had my doubts before, but now that you've posted everything, here's what's up. Assuming the RestTemplate object you use in your getGreetingMessage() method is the same as the one declared in the @Bean method, the problem starts here

this.mockServer = MockRestServiceServer.createServer(restTemplate);

This call overwrites the default ClientHttpRequestFactory object that the RestTemplate object uses internally with a mock. In your getGreetingMessage() method, this call

ResponseEntity<GreetingResource> responseEntity = restTemplate.getForEntity("/admin/greeting", GreetingResource.class, httpHeaders);

doesn't actually go through the network. The RestTemplate uses the mocked ClientHttpRequestFactory to create a fake ClientHttpRequest which produces a fake ClientHttpResponse which doesn't have a Content-Type header. When the RestTemplate looks at the ClientHttpResponse to determine its Content-Type and doesn't find one, it assumes application/octet-stream by default.

So, your controller isn't setting the content type because your controller is never hit. The RestTemplate is using a default content type for your response because it is mocked and doesn't actually contain one.


From your comments:

I wonder if I understand what the mock server is testing. I understand it is to be used in acceptance testing scenario. Is it supposed to hit the controller at all ?

The javadoc for MockRestServiceServer states:

Main entry point for client-side REST testing. Used for tests that involve direct or indirect (through client code) use of the RestTemplate. Provides a way to set up fine-grained expectations on the requests that will be performed through the RestTemplate and a way to define the responses to send back removing the need for an actual running server.

In other words, it's as if your application server didn't exist. So you could throw any expectations (and actual return values) you wanted and test whatever happens from the client side. So you aren't testing your server, you are testing your client.

Are you sure you aren't looking for MockMvc, which is

Main entry point for server-side Spring MVC test support.

which you can setup to actually use your @Controller beans in an integration environment. You aren't actually sending HTTP request, but the MockMvc is simulating how they would be sent and how your server would respond.

OTHER TIPS

It is bug in MockHttpServletRequest and I will try to describe it. Issue in tracker https://jira.springsource.org/browse/SPR-11308#comment-97327 Fixed in version 4.0.1

Bug

When DispatcherServlet looking for method to invoke it using some RequestConditions. One of them is ConsumesRequestCondition. The following is a piece of code:

@Override
    protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotSupportedException {
        try {
            MediaType contentType = StringUtils.hasLength(request.getContentType()) ?
                    MediaType.parseMediaType(request.getContentType()) :
                    MediaType.APPLICATION_OCTET_STREAM;
            return getMediaType().includes(contentType);
        }
        catch (IllegalArgumentException ex) {
            throw new HttpMediaTypeNotSupportedException(
                    "Can't parse Content-Type [" + request.getContentType() + "]: " + ex.getMessage());
        }
    }

We are interested in piece request.getContentType(). There request is MockHttpServletRequest. Let's look on method getContentType():

public String getContentType() {
    return this.contentType;
}

It just return value of this.contentType. It does not return a value from the header! And this.contentType is always NULL. Then contentType in matchMediaType methos will be always MediaType.APPLICATION_OCTET_STREAM.

Solution

I have tried many ways but have found only one that works.

  • Create package org.springframework.test.web.client in your test directory.
  • Create copy of org.springframework.test.web.client.MockMvcClientHttpRequestFactory but rename it. For example rename to FixedMockMvcClientHttpRequestFactory.
  • Find line:

    MvcResult mvcResult = MockMvcClientHttpRequestFactory.this.mockMvc.perform(requestBuilder).andReturn();
    
  • Replace it with code:

    MvcResult mvcResult = FixedMockMvcClientHttpRequestFactory.this.mockMvc.perform(new RequestBuilder() {
        @Override
        public MockHttpServletRequest buildRequest(ServletContext servletContext) {
            MockHttpServletRequest request = requestBuilder.buildRequest(servletContext);
            request.setContentType(request.getHeader("Content-Type"));
            return request;
        }
    }).andReturn();
    
  • And register your ClientHttpReque

    @Bean
    public ClientHttpRequestFactory clientHttpRequestFactory(MockMvc mockMvc) {
        return new FixedMockMvcClientHttpRequestFactory(mockMvc);
    }
    

I know that it is not beautiful solution but it works fine.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top