I have a Search and Users microservices. Search uses Spring Feign to call an endpoint in Users' UserController that searches for the users and returns those that match the query.
The endpoint in Search's SearchController receives query, page, sort & size parameters, of which the last three are automatically encapsulated in a Pageable object. Query and Pageable object are passed in the request body to the Users microservice.
The problem is that Spring Feign is not adding the Pageable object into the request body and then sending a request to http://users/search (as it should), but is instead decomposing the Pageable object and sending the 3 parameters as url parameters. Everything fails because UsersFeignClient is making a request to http://users/search?size=10&page=0&sort=username%CASC for which the endpoint does not exist, instead to http://users/search and passing query and pageable in the body.
SearchController's searchUsers method
@GetMapping("/users")
public ResponseEntity<PagedModel<EntityModel<UserDto>>> searchUsers(
@RequestParam(name = "q") String query,
@PageableDefault(sort = "username", direction = Sort.Direction.ASC) Pageable pageable) {
PagedModel<EntityModel<UserDto>> page = searchService.searchUsers(query, pageable);
logger.info("Returning a page {}/{} of users", page.getMetadata().getNumber(), page.getMetadata().getTotalPages());
return ResponseEntity.ok(page);
}
SearchService's searchUsers method
@Override
public PagedModel<EntityModel<UserDto>> searchUsers(String query, Pageable pageable) {
logger.info("Searching for usernames that match query: {}", query);
return usersFeignClient.searchUsers(query, pageable);
}
UsersFeignClient
@FeignClient(name = "users", value = "users", configuration = AppConfiguration.class)
public interface UsersFeignClient {
@RequestMapping(method = RequestMethod.POST, value = "/api/users/search",
consumes = MediaType.APPLICATION_JSON_VALUE)
PagedModel<EntityModel<UserDto>> searchUsers(@RequestBody String query, @RequestBody Pageable pageable);
}
AppConfiguration
@Configuration
public class AppConfiguration {
/*@Bean
public Encoder encoder(ObjectFactory<HttpMessageConverters> converters) {
return new SpringFormEncoder(new SpringEncoder(converters));
}*/
@Bean
@Primary
public Encoder encoder(ObjectMapper objectMapper, PageableSpringEncoder pageableSpringEncoder) {
return new CustomEncoder(pageableSpringEncoder, objectMapper);
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public PageableSpringEncoder pageableSpringEncoder(ObjectFactory<HttpMessageConverters> converters) {
return new PageableSpringEncoder(new SpringEncoder(converters));
}
}
CustomEncoder
public class CustomEncoder implements Encoder {
private final PageableSpringEncoder pageableSpringEncoder;
private final ObjectMapper objectMapper;
public CustomEncoder(PageableSpringEncoder pageableSpringEncoder, ObjectMapper objectMapper) {
this.pageableSpringEncoder = pageableSpringEncoder;
this.objectMapper = objectMapper;
}
@Override
public void encode(Object o, Type type, RequestTemplate requestTemplate) throws EncodeException {
if (o instanceof Pageable) {
Pageable pageable = (Pageable) o;
String json = null;
try {
json = objectMapper.writeValueAsString(pageable);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
requestTemplate.body(json);
} else {
pageableSpringEncoder.encode(o, type, requestTemplate);
}
}
}
UserController's search method
@PostMapping("/search")
public PagedModel<EntityModel<UserDto>> searchUsers(@RequestBody String query,
@RequestBody Pageable pageable,
PagedResourcesAssembler<UserDto> assembler) {
Page<UserDto> page = userService.search(query, pageable);
PagedModel<EntityModel<UserDto>> pagedModel = assembler.toModel(page, userModelAssembler);
logger.info("Returning a page {}/{} of users", page.getNumber(), page.getTotalPages());
return pagedModel;
}
Error message
feign.FeignException$BadRequest: [400] during [POST] to [http://users/api/users/search?size=10&page=0&sort=username%2CASC] [UsersFeignClient#searchUsers(String,Pageable)]: [{"type":"about:blank","title":"Bad Request","status":400,"detail":"Failed to read request","instance":"/api/users/search"}]
What did I try:
- I tried creating an Encoder bean and adding it to UsersFeignClient
- I tried creating CustomEncoder and adding it as a bean to UsersFeignClient.
- Went through the docs.
- Added spring-data-commons dependency
- Switched the second @RequestBody to @SpringQueryMap in the UsersFeignClient:
@FeignClient(name = "users", value = "users"/*, configuration = AppConfiguration.class*/)
public interface UsersFeignClient {
@RequestMapping(method = RequestMethod.POST, value = "/api/users/search",
consumes = MediaType.APPLICATION_JSON_VALUE)
PagedModel<EntityModel<UserDto>> searchUsers(@RequestBody String query, @SpringQueryMap Pageable pageable);
}
Nothing worked.
The only solution I see is changing the UserController's search method to accept url parameters instead Pageable in the body.
But I just don't understand is this the problem that I've created or Spring does not provide support for injecting Peageables into request body.
Any clarification/help/advice is appreciated.