Spring WebClient - How to handle the errors on the body Rest - java

I'm a newbie with Spring Boot, and I need your help.
I make a GET request with WebClient, and I receive a JSON body as below:
{
"status": "OK",
"error": [],
"payload": {
"name": "John",
"surname": "Doe"
...
}
}
So I have a DTO class in which mapping the response. Something like this:
#Data
#AllArgsConstructor
#NoArgsConstructor
public class ResponseAccountDTO {
private String status;
private List<ErrorDTO> errors;
private User payload;
}
I do it whit this method:
public ResponseUserDTO retrieveUserById(String userId) {
return webClient.get()
.uri(GET_USER_BY_ID_V4, accountId)
.header("Auth-Schema", AUTH_SCHEMA)
.header("apikey", API_KEY)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> {
System.out.println("4xx error");
return Mono.error(new RuntimeException("4xx"));
})
.onStatus(HttpStatus::is5xxServerError, response -> {
System.out.println("5xx error");
return Mono.error(new RuntimeException("5xx"));
})
.bodyToMono(ResponseDTO.class)
.block();
}
Finally, I test it with this method:
UserRestClient userRestClient = new UserRestClient(webClient);
#Test
void retrieveUser() {
ResponseDTO response = userRestClient.retrieveUserById("123");
UserDTO user = response.getPayload();
System.out.println("user surname: " + user.surname);
assertEquals("Doe", user.getSurname());
}
All fine until the response has KO Status. If something goes wrong (i.e., BAD REQUEST), I receive the same body JSON structure, as below:
{
"status": "KO",
"errors": [
{
"code": "ER000",
"description": "Wrong ID parameter",
"params": ""
}
],
"payload": {}
}
Is there a way to map also with KO Status the JSON body on my DTO class?
I want to return the error description on my retrieveUser() method.
Update:
I add my ErrorDTO class as suggest by Seelenvirtuose
#Data
#AllArgsConstructor
#NoArgsConstructor
public class ErrorDTO {
private String code;
private String description;
private String params;
}

I myself ran into this issue and had to convert the json error response to an ErrorDTO object.
Hope the below code helps you for what you are looking for.
The below code can be applied to any Status code (e.g. 4xx, 5xx and even for 2xx as well but you won't need it for 2xx)
.onStatus(HttpStatus::is4xxClientError, error -> error
.bodyToMono(Map.class)
.flatMap(body -> {
try {
var message = objectMapper.writeValueAsString(body);
ErrorDTO errorResponse = objectMapper.readValue(message, ErrorDTO.class);
return Mono.error(new ServiceException(error.statusCode().value(), "My custom error message", errorResponse));
} catch (JsonProcessingException jsonProcessingException) {
return Mono.error(new ServiceException("Cannot parse the error response"));
}
})
)

Related

Spring Boot BindingResult returns 2 same error messages on one field

I tried using annotation #NotNull and #Past with my field for validation, but when I posted with null value in PostMan, it gave me 2 error responses instead of 1, is this a problem and if it is, how to fix it?
Error Handler:
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(final MethodArgumentNotValidException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) {
logger.info(ex.getClass().getName());
final Map<String, String> errors = new HashMap<>();
for (final FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), String.format("%s %s", error.getField(), error.getDefaultMessage()));
}
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
The field that needs to validate:
#Past
#NotNull
private Date birthDate;
Request body:
{
"birthDate":""
}
Response:
[
{
"field": "birthDate",
"message": "must not be null"
},
{
"field": "birthDate",
"message": "must not be null"
}
]

Parse JSON with Spring WebFlux after error occured

I'm receiving JSON from REST API looks like:
{
"items": [
{
"id": 60659,
"name": "Display",
"active": true,
"account_id": 235
},
{
"id": 36397,
"name": " Mail Display",
"active": true,
"account_id": 107
}
]
}
I'm using this method to parse it:
Mono<List<Item>> getItems(String token) {
return webCLient
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(token))
.retrieve()
.bodyToMono(ItemResponse.class)
.map(ItemResponse::getResponse)
.retryBackoff(RetrySettings.RETRIES, RetrySettings.FIRST_BACKOFF, RetrySettings.MAX_BACKOFF)
.doOnError(e -> log.error("error: " + e.getCause().toString()))
Response:
public class ItemResponse {
#JsonProperty("items")
private List<Item> response;
}
But sometimes 3rd party API returns different response without top level items property and looks like:
[
{
"id": 60659,
"name": "Display",
"active": true,
"account_id": 235
},
{
"id": 36397,
"name": " Mail Display",
"active": true,
"account_id": 107
}
]
At this point my app is crashing with JSON decoding error. I used for this case:
bodyToMono(new ParameterizedTypeReference<List<Item>>() {})
But I can't always refactoring this part of code just to handle their json. How to do it in dynamical way with Spring WebFlux? Like try -> parse#1 -> catch -> parse#2. So i need to parse json in way#1 and if error occurs app should try to parse it with way#2.
You can get the response as a string .bodyToMono(String.class) and do whatever you want, with multiple try catches... but I think your best bet is to create a custom Deserializer and use it with your WebClient via ExchangeStrategies like described here: How to customize SpringWebFlux WebClient JSON deserialization?
.
class MyResponse {
List<Object> data;
MyResponse(List<Object> data) {
this.data = data;
}
}
class MyResponseDeserializer extends JsonDeserializer<MyResponse> {
#Override
public MyResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser);
List<Object> data = new ArrayList<>();
if (treeNode.isArray()) {
// parse it as array
} else {
// parse it as object and put inside list
}
MyResponse myResponse = new MyResponse(data);
return myResponse;
}
}
And then
WebClient getWebClient() {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(MyResponse.class, new MyResponseDeserializer());
objectMapper.registerModule(simpleModule);
ExchangeStrategies strategies = ExchangeStrategies
.builder()
.codecs(clientDefaultCodecsConfigurer -> {
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON));
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON));
}).build();
return WebClient.builder().exchangeStrategies(strategies).build();
}
Mono<List<Item>> getItems(String token) {
return getWebClient()
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(token))
.retrieve()
.bodyToMono(MyResponse.class)
.map(MyResponse::data)
.retryBackoff(RetrySettings.RETRIES, RetrySettings.FIRST_BACKOFF, RetrySettings.MAX_BACKOFF)
.doOnError(e -> log.error("error: " + e.getCause().toString()))
}
The rest is the same as in your example just change the class name and add appropriate fields.
And of course this is just a fast written demo and everything hardcoded and within a one method, better to have them injected

How to customize GraphQL query validation error message

I'm implementing query layer on database by using GraphQl and spring boot project to perform CRUD operation on sql database. In GraphQL schema i mentioned some fields to be mandatory and when those fields are not mentioned in query it is returning ValidationError error message in default format with 200 status code.
Error :
{
"data": null,
"errors": [
{
value=StringValue{value='1235'}}]}}]}' is missing required fields '[book_type]' # 'create_book'",
"locations": [
{
"line": 3,
"column": 23,
"sourceName": null
}
],
"description": "argument 'insert' with value value=StringValue{value='1235'}}]}}]}' is missing required fields '[book_type]'",
"validationErrorType": "WrongType",
"queryPath": [
"create_book"
],
"errorType": "ValidationError",
"path": null,
"extensions": null
}
],
"dataPresent": false,
"extensions": null
}
And here is my code with layer architecture pattern
Controller :
#Autowired
private GraphQLServer graphQlServer;
#PostMapping("test")
public ResponseEntity<Object> graphQl(#RequestBody String body){
ExecutionResult response = graphQlServer.execute(body);
return ResponseEntity.ok(response);
}
Service :
#Service
public class GraphQLServer {
#Autowired
private GraphQL graphQl;
public ExecutionResult execute(String query) {
return graphQl.execute(query);
}
}
Config :
#Bean
public GraphQL loadSchema() throws IOException {
File schemaFile = schemaResource.getFile();
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(schemaFile);
RuntimeWiring wiring = buildRuntimeWiring();
GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(typeRegistry, wiring);
return GraphQL.newGraphQL(schema).build();
}
private RuntimeWiring buildRuntimeWiring() {
return RuntimeWiring.newRuntimeWiring()
.type("Mutation", mutationWiring -> mutationWiring.dataFetcher("create_book", bookDataFetcher))
.build();
}
BookDataFetcher :
#Override
public Map<String, Object> get(DataFetchingEnvironment environment) {
//return data from db by getting Map properties from environment
}
The above code is working as expected but my question here is How to customize the error message? In the error message i would like to mention the status 400 since it is bad request from client
First of all , you should call toSpecification() on ExecutionResult to make sure the response obeys the GraphQL Specification.
By default , there is only one ExecutionResult 's implementation provided by graphql-java which is ExecutionResultImpl , so you can cast ExecutionResult to it in order to use its transform() to update its state.
ExecutionResultImpl internally contains all errors detected by the graphql-java. All of them are in the subclass of GraphQLError which mean you have to cast it to the specific sub-class during customization.
In your case , the subclass is ValidationError and the codes look something like :
#PostMapping("test")
public ResponseEntity<Object> graphQl(#RequestBody String body){
ExecutionResult response = graphQlServer.execute(body);
ExecutionResultImpl responseImpl = (ExecutionResultImpl) response;
List<GraphQLError> customizedErrors = Lists.newArrayList();
for (GraphQLError gqlError : responseImpl.getErrors()) {
//Do your error custmosation here....
GraphQLError customizedError = gqlError;
if (gqlError instanceof ValidationError) {
ValidationError error = (ValidationError) gqlError;
customizedError = new ValidationError(error.getValidationErrorType(), error.getLocations(),
"Customizing some error message blablabla....");
}
customizedErrors.add(customizedError);
}
Map<String, Object> specResponse = responseImpl.transform(b->b.errors(customizedErrors)).toSpecification();
return ResponseEntity.ok(specResponse);
}

Spring boot controller - Download Multipart and JSON to DTO

I need get a JAVA Object (DTO) with a MultipartFile in a Spring Controller
I tried different ways, like use of produces = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE} but nothing works.
The "DTO" to get is:
public class TodoDTO {
private Long id;
private String description;
private Boolean status;
private MultipartFile image;
...
}
And the controller method is:
#GetMapping(produces = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<List<TodoDTO>> getAll() {
ResponseEntity<List<TodoDTO>> response;
try {
response = new ResponseEntity<List<TodoDTO>>(todoService.getAll(), HttpStatus.FOUND);
} catch (Exception e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage(), e);
}
return response;
}
I will expect to get entire object, with Multipart and the other properties. But the response in Postman is:
"status": 406,
"error": "Not Acceptable",
"message": "Could not find acceptable representation",

Spring ResponsEntity body contains extra json with timestamp, status and more

We have an REST endpoint that will add a new empty ingredient to an existing meal:
#RequestMapping(value = "/add", method = RequestMethod.PUT, consumes = "application/json;charset=UTF-8")
public ResponseEntity<Object> add(#RequestBody final Meal meal) throws URISyntaxException
{
Optional<Meal> optionalMeal = mealRepository.findById(meal.getId());
if (!optionalMeal.isPresent()) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(MessageUtil.parse(MSG_404_MEAL, meal.getId() + ""));
}
Ingredient ingredient = new Ingredient();
ingredient.setMeal(optionalMeal.get());
ingredientRepository.saveAndFlush(ingredient);
ResponseEntity re = ResponseEntity
.created(RequestUtil.getResourceURI(ingredient.getId()))
.body(ingredient);
return re;
}
Ingredient is an entity class with some fields:
public class Ingredient implements Serializable
{
#Id
private Integer id;
private Meal meal;
private Grocery grocery;
private Float amount;
...
}
RequestUtil takes care of creating the URI where the newly created resource is to be found:
public class RequestUtil
{
public static URI getResourceURI(int id) throws URISyntaxException
{
final String url = RequestUtil.getCurrentRequest().getRequestURL().toString();
final String req = RequestUtil.omitLast(url);
return new URI(req + "get/" + id);
}
public static HttpServletRequest getCurrentRequest()
{
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return ((ServletRequestAttributes) requestAttributes).getRequest();
}
public static String omitLast(final String url) {
return url.substring(0, url.lastIndexOf("/") + 1);
}
}
The http status code and resource URI end up correctly in the response headers, but the body contains two JSONs:
{
"id": 407,
"meal": {
"id": 99,
"name": "New Meal",
"active": true
},
"grocery": null,
"amount": null,
"bought": false
} {
"timestamp": "2018-08-29T19:25:31.466+0000",
"status": 201,
"error": "Created",
"message": "No message available",
"path": "/ingredient/add"
}
Our javascript code does not expect this extra data and fails with
SyntaxError: JSON.parse: unexpected non-whitespace character after JSON data at line 1 column 114 of the JSON data
Using a debugger, we can see that by the time the code reaches the return statement in add(), the ResponseEntity does not contain this extra data. Can someone explain where it comes from, and how we stop it from polluting the response?
Thanks for any help!

Categories

Resources