Reputation: 12800
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
}
Upvotes: 0
Views: 43317
Reputation: 1160
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
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
.
I have tried many ways but have found only one that works.
org.springframework.test.web.client
in your test directory.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.
Upvotes: 1
Reputation: 280138
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 theRestTemplate
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.
Upvotes: 5