Spring json error 400

I am working on a REST api. Receiving a POST message with bad JSON (e.g. { sdfasdfasdf } ) causes Spring to return the default server page for a 400 Bad Request Error. I do not want to return a p...

I am working on a REST api. Receiving a POST message with bad JSON (e.g. { sdfasdfasdf } ) causes Spring to return the default server page for a 400 Bad Request Error. I do not want to return a page, I want to return a custom JSON Error object.

I can do this when there is an exception thrown by using an @ExceptionHandler. So if it is a blank request or a blank JSON object (e.g. { } ), it will throw a NullPointerException and I can catch it with my ExceptionHandler and do whatever I please.

The problem then, is that Spring doesn’t actually throw an exception when it is just invalid syntax… at least not that I can see. It simply returns the default error page from the server, whether it is Tomcat, Glassfish, etc.

So my question is how can I «intercept» Spring and cause it to use my exception handler or otherwise prevent the error page from displaying and instead return a JSON error object?

Here is my code:

@RequestMapping(value = "/trackingNumbers", method = RequestMethod.POST, consumes = "application/json")
@ResponseBody
public ResponseEntity<String> setTrackingNumber(@RequestBody TrackingNumber trackingNumber) {

    HttpStatus status = null;
    ResponseStatus responseStatus = null;
    String result = null;
    ObjectMapper mapper = new ObjectMapper();

    trackingNumbersService.setTrackingNumber(trackingNumber);
    status = HttpStatus.CREATED;
    result = trackingNumber.getCompany();


    ResponseEntity<String> response = new ResponseEntity<String>(result, status);

    return response;    
}

@ExceptionHandler({NullPointerException.class, EOFException.class})
@ResponseBody
public ResponseEntity<String> resolveException()
{
    HttpStatus status = null;
    ResponseStatus responseStatus = null;
    String result = null;
    ObjectMapper mapper = new ObjectMapper();

    responseStatus = new ResponseStatus("400", "That is not a valid form for a TrackingNumber object " + 
            "({"company":"EXAMPLE","pro_bill_id":"EXAMPLE123","tracking_num":"EXAMPLE123"})");
    status = HttpStatus.BAD_REQUEST;

    try {
        result = mapper.writeValueAsString(responseStatus);
    } catch (IOException e1) {
        e1.printStackTrace();
    }

    ResponseEntity<String> response = new ResponseEntity<String>(result, status);

    return response;
}

Содержание

  1. Обработка исключений Spring Boot REST API — ResponseEntityExceptionHandler
  2. Приложение
  3. Класс обработки исключений
  4. HttpMessageNotReadableException
  5. MethodArgumentNotValidException
  6. MethodArgumentTypeMismatchException
  7. NoHandlerFoundException
  8. Обработчик по умолчанию
  9. Заключение
  10. Добавить комментарий Отменить ответ
  11. How to send a custom error message for bad requests (400 error code) in Spring Boot?

Обработка исключений Spring Boot REST API — ResponseEntityExceptionHandler

В части 1 мы рассмотрели варианты обработки исключений, выбрасываемых в контроллере.

Самый гибкий из них — @ControllerAdvice — он позволяет изменить как код, так и тело стандартного ответа при ошибке. Кроме того, он позволяет в одном методе обработать сразу несколько исключений — они перечисляются над методом.

В первой части мы создавали @ControllerAdvice с нуля, но в Spring Boot существует заготовка — ResponseEntityExceptionHandler, которую можно расширить. В ней уже обработаны многие исключения, например: NoHandlerFoundException, HttpMessageNotReadableException, MethodArgumentNotValidException и другие (всего десяток-другой исключений).

Приложение

Обрабатывать исключения будем в простом Spring Boot приложении из первой части. Оно предоставляет REST API для сущности Person:

Только в этот раз поле name аннотировано javax.validation.constraints.Size.

А также перед аргументом Person в методах контроллера стоит аннотация @Valid:

Аннотация @Valid заставляет Spring проверять валидность полей объекта Person, например условие @Size(min = 3, max = 10). Если пришедший в контроллер объект не соответствует условиям, то будет выброшено MethodArgumentNotValidException — то самое, для которого в ResponseEntityExceptionHandler уже задан обработчик. Правда, он выдает пустое тело ответа. Вообще все обработчики из ResponseEntityExceptionHandler выдают корректный код ответа, но пустое тело.

Мы это исправим. Поскольку для MethodArgumentNotValidException может возникнуть несколько ошибок (по одной для каждого поля сущности Person), добавим в наше пользовательское тело ответа список List с ошибками. Он предназначен именно для MethodArgumentNotValidException (не для других исключений).

Итак, ApiError по сравнению с 1-ой частью теперь содержит еще список errors:

Благодаря аннотации @JsonInclude(JsonInclude.Include.NON_NULL) этот список будет включен в ответ только в том случае, если мы его зададим. Иначе ответ будет содержать только message и debugMessage, как в первой части.

Класс обработки исключений

Например, на исключение MyEntityNotFoundException ответ не поменяется, обработчик такой же, как в первой части:

Но в отличие от 1 части, теперь RestExceptionHandler расширяет ResponseEntityExceptionHandler. А значит, он наследует различные обработчики исключений, и мы их можем переопределить. Сейчас они все возвращают пустое тело ответа, хотя и корректный код.

HttpMessageNotReadableException

Переопределим обработчик, отвечающий за HttpMessageNotReadableException. Это исключение возникает тогда, когда тело запроса, приходящего в метод контроллер, нечитаемое — например, некорректный JSON.

За это исключение отвечает метод handleHttpMessageNotReadable(), его и переопределим:

Проверим ответ, сделав запрос с некорректным JSON-телом запроса (он пойдет в метод updatePerson() контроллера):

Получаем ответ с кодом 400 (Bad Request) и телом:

Теперь ответ содержит не только корректный код, но и тело с информативными сообщениями. Если бы мы не переопределяли обработчик, вернулся бы только код 400.

А если бы не расширяли класс ResponseEntityExceptionHandler, все эти обработчики в принципе не были бы задействованы и вернулся бы стандартный ответ из BasicErrorController:

MethodArgumentNotValidException

Как говорилось выше, чтобы выбросилось это исключение, в контроллер должен прийти некорректный Person. В смысле корректный JSON, но условие @Valid чтоб не выполнялось: например, поле name имело бы неверную длину (а она должна быть от 3 до 10, как указано в аннотации @Size).

Попробуем сделать запрос с коротким name:

Тут пошел в ход список ошибок, который мы добавили в ApiError. Мы его заполняем в переопределенном обработчике исключения:

Вообще говоря, стандартный ответ, выдаваемый BasicErrorController, тоже будет содержать этот список ошибок по полям, если в application.properties включить свойство:

В этом случае (при отсутствии нашего RestExceptionHandler с @ControlleAdvice) ответ будет таким:

Мы просто сократили информацию.

MethodArgumentTypeMismatchException

Полезно знать еще исключение MethodArgumentTypeMismatchException, оно возникает, если тип аргумента неверный. Например, наш метод контроллера получает Person по id:

А мы передаем не целое, а строковое значение id:

Тут то и возникает исключение MethodArgumentTypeMismatchException. Давайте его обработаем:

Проверим ответ сервера (код ответа будет 400):

NoHandlerFoundException

Еще одно полезное исключение — NoHandlerFoundException. Оно возникает, если на данный запрос не найдено обработчика.

Например, сделаем запрос:

По данному адресу у нас нет контроллера, так что возникнет NoHandlerFoundException. Добавим обработку исключения:

Только учтите, для того, чтобы исключение выбрасывалось, надо задать свойства в файле application.properties:

Проверим ответ сервера (код ответа 404):

Если же не выбрасывать NoHandlerFoundException и не пользоваться нашим обработчиком, то ответ от BasicErrorController довольно непонятный, хотя код тоже 404:

Обработчик по умолчанию

Этот обработчик будет ловить исключения, не пойманные предыдущими обработчиками:

Заключение

  • как сделать обработку исключений в едином классе, аннотированном @ControllerAdvice;
  • как переопределить формат JSON-ответа, выдаваемого при возникновении исключения;
  • как воспользоваться классом-заготовкой ResponseEntityExceptionHandler и переопределить его обработчики так, чтобы тело ответов не было пустым;

Обратите внимание, что все не переопределенные методы ResponseEntityExceptionHandler будут выдавать пустое тело ответа.

Код примера доступен на GitHub.

Добавить комментарий Отменить ответ

Прошу прощения: на комментарии временно не отвечаю.

Источник

How to send a custom error message for bad requests (400 error code) in Spring Boot?

Let’s say you created a REST API which accepts a JSON input.

You handle any type of exception in your code and throw a custom error message to the user.

But what if the user sends a bad JSON ?

Spring framework will throw a predefined error message.

Below is the error message I got for supplying invalid JSON to a sample REST API I created:

In some cases different error messages could be returned .

Here is the REST API:

As you see I am just receiving the input and storing it another map along with the status and returning it back.

For any exception I am throwing a custom error message “Your request is invalid”.

What if I want to throw the same error message on a bad request.

It can’t be handled directly in the code as every exception is already caught using a try catch block inside the controller class.

This can be achieved by creating a Controller Advice.

Here is the controller advice class I created:

To throw a custom error message follow the below algorithm:

STEP1: Create a controller advice class

STEP2: Annotate it with @ControllerAdvice

STEP3: Create a method and return the custom error message in a map inside the method

STEP4: Annotate the method with @ExceptionHandler as this method handles exceptions

STEP5: Annotate the same method with @ResponseStatus(HttpStatus.BAD_REQUEST) as it deals with only 400 BAD REQUEST exceptions

STEP6: Annotate the same method with @ResponseBody to indicate that the response body to be returned for bad requests is supplied by this method.

After implementing it , when I hit the REST API it threw the custom error defined:

Источник

Let’s say you created a REST API which accepts a JSON input.

You handle any type of exception in your code and throw a custom error message to the user.

But what if the user sends a bad JSON ?

Spring framework will throw a predefined error message.

Below is the error message I got for supplying invalid JSON to a sample REST API I created:

In some cases different error messages could be returned .

Here is the REST API:

package com.badrequest.custommessage;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.HashMap;
import java.util.Map;

@org.springframework.web.bind.annotation.RestController
public class RestController {



    @PostMapping("/poke")
    public Map<String,Object> poke(@RequestBody  Map<String,Object> request){

        Map<String,Object> response = new HashMap<String,Object>();

        try{


            response.put("request",request);
            response.put("status","success");


        }catch(Exception e){

            response.put("status","Your request is invalid");

        }

        return response;

    }
}

As you see I am just receiving the input and storing it another map along with the status and returning it back.

For any exception I am throwing a custom error message “Your request is invalid”.

What if I want to throw the same error message on a bad request.

It can’t be handled directly in the code as every exception is already caught using a try catch block inside the controller class.

This can be achieved by creating a Controller Advice.

Here is the controller advice class I created:

package com.badrequest.custommessage;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class ExceptionHandler {

    @org.springframework.web.bind.annotation.ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    Map<String,String> showCustomMessage(Exception e){


        Map<String,String> response = new HashMap<>();
        response.put("status","Your input is invalid");

        return response;
    }
}

To throw a custom error message follow the below algorithm:

STEP1: Create a controller advice class

STEP2: Annotate it with @ControllerAdvice

STEP3: Create a method and return the custom error message in a map inside the method

STEP4: Annotate the method with @ExceptionHandler as this method handles exceptions

STEP5: Annotate the same method with @ResponseStatus(HttpStatus.BAD_REQUEST) as it deals with only 400 BAD REQUEST exceptions

STEP6: Annotate the same method with @ResponseBody to indicate that the response body to be returned for bad requests is supplied by this method.

After implementing it , when I hit the REST API it threw the custom error defined:

That’s it!

В части 1 мы рассмотрели варианты обработки исключений, выбрасываемых в контроллере.

Самый гибкий из них — @ControllerAdvice — он позволяет изменить как код, так и тело стандартного ответа при ошибке. Кроме того, он позволяет в одном методе обработать сразу несколько исключений — они перечисляются над методом.

В первой части мы создавали @ControllerAdvice с нуля, но в Spring Boot существует заготовка — ResponseEntityExceptionHandler, которую можно расширить. В ней уже обработаны многие исключения, например: NoHandlerFoundException, HttpMessageNotReadableException, MethodArgumentNotValidException и другие (всего десяток-другой исключений).

Приложение

Обрабатывать исключения будем в простом Spring Boot приложении из первой части. Оно предоставляет REST API для сущности Person:

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Size(min = 3, max = 10)
    private String name;
    
}

Только в этот раз поле name аннотировано javax.validation.constraints.Size.

А также перед аргументом Person в методах контроллера стоит аннотация @Valid:

@RestController
@RequestMapping("/persons")
public class PersonController {

    @Autowired
    private PersonRepository personRepository;

    @GetMapping
    public List<Person> listAllPersons() {
        List<Person> persons = personRepository.findAll();
        return persons;
    }

    @GetMapping(value = "/{personId}")
    public Person getPerson(@PathVariable("personId") long personId) {
        return personRepository.findById(personId).orElseThrow(() -> new MyEntityNotFoundException(personId));
    }

    @PostMapping
    public Person createPerson(@RequestBody @Valid Person person) {
        return personRepository.save(person);
    }

    @PutMapping("/{id}")
    public Person updatePerson(@RequestBody @Valid Person person, @PathVariable long id) {
        Person oldPerson = personRepository.getOne(id);
        oldPerson.setName(person.getName());
        return personRepository.save(oldPerson);
    }

}

Аннотация @Valid заставляет Spring проверять валидность полей объекта Person, например условие @Size(min = 3, max = 10). Если пришедший в контроллер объект не соответствует условиям, то будет выброшено MethodArgumentNotValidException — то самое, для которого в ResponseEntityExceptionHandler уже задан обработчик. Правда, он выдает пустое тело ответа. Вообще все обработчики из ResponseEntityExceptionHandler выдают корректный код ответа, но пустое тело.

Мы это исправим. Поскольку для MethodArgumentNotValidException может возникнуть несколько ошибок (по одной для каждого поля сущности Person), добавим в наше пользовательское тело ответа список List с ошибками. Он предназначен именно для MethodArgumentNotValidException (не для других исключений).

Итак, ApiError по сравнению с 1-ой частью теперь содержит еще список errors:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiError {
    private String message;
    private String debugMessage;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private List<String> errors;

    public ApiError(String message, String debugMessage){
        this.message=message;
        this.debugMessage=debugMessage;
    }
}

Благодаря аннотации @JsonInclude(JsonInclude.Include.NON_NULL) этот список будет включен в ответ только в том случае, если мы его зададим. Иначе ответ будет содержать только message и debugMessage, как в первой части.

Класс обработки исключений

Например, на исключение MyEntityNotFoundException ответ не поменяется, обработчик такой же, как в первой части:

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

   ...

    @ExceptionHandler({MyEntityNotFoundException.class, EntityNotFoundException.class})
    protected ResponseEntity<Object> handleEntityNotFoundEx(MyEntityNotFoundException ex, WebRequest request) {
        ApiError apiError = new ApiError("Entity Not Found Exception", ex.getMessage());
        return new ResponseEntity<>(apiError, HttpStatus.NOT_FOUND);
    }
   ...
}

Но в отличие от 1 части, теперь RestExceptionHandler расширяет ResponseEntityExceptionHandler.  А значит, он наследует различные обработчики исключений, и мы их можем переопределить. Сейчас они все возвращают пустое тело ответа, хотя и корректный код.

HttpMessageNotReadableException

Переопределим обработчик, отвечающий за HttpMessageNotReadableException. Это исключение возникает тогда, когда тело запроса, приходящего в метод контроллер, нечитаемое — например, некорректный JSON.

За это исключение отвечает метод handleHttpMessageNotReadable(), его и переопределим:

@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
                                                              HttpHeaders headers, HttpStatus status, WebRequest request) {
    ApiError apiError = new ApiError("Malformed JSON Request", ex.getMessage());
    return new ResponseEntity(apiError, status);
}

Проверим ответ, сделав запрос с некорректным JSON-телом запроса (он пойдет в метод updatePerson() контроллера):

PUT localhost:8080/persons/1
{
   11"name": "alice"
}

Получаем ответ с кодом 400 (Bad Request) и телом:

{
    "message": "Malformed JSON Request",
    "debugMessage": "JSON parse error: Unexpected character ('1' (code 49)): was expecting double-quote to start field name; nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('1' (code 49)): was expecting double-quote to start field namen at [Source: (PushbackInputStream); line: 2, column: 5]"
}

Теперь ответ содержит не только корректный код, но и тело с информативными сообщениями. Если бы мы не переопределяли обработчик, вернулся бы только код 400.

А если бы не расширяли класс ResponseEntityExceptionHandler, все эти обработчики в принципе не были бы задействованы и вернулся бы стандартный ответ из BasicErrorController:

{
    "timestamp": "2021-03-01T16:53:04.197+00:00",
    "status": 400,
    "error": "Bad Request",
    "message": "JSON parse error: Unexpected character ('1' (code 49)): was expecting double-quote to start field name; nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('1' (code 49)): was expecting double-quote to start field namen at [Source: (PushbackInputStream); line: 2, column: 5]",
    "path": "/persons/1"
}

MethodArgumentNotValidException

Как говорилось выше, чтобы выбросилось это исключение, в контроллер должен прийти некорректный Person. В смысле корректный JSON, но условие @Valid чтоб не выполнялось: например, поле name имело бы неверную длину (а она должна быть от 3 до 10, как указано в аннотации @Size).

Попробуем сделать запрос с коротким name:

POST http://localhost:8080/persons
{ 
   "name": "al" 
}

Получим ответ:

{
    "message": "Method Argument Not Valid",
    "debugMessage": "Validation failed for argument [0] in public ru.sysout.model.Person ru.sysout.controller.PersonController.createPerson(ru.sysout.model.Person): [Field error in object 'person' on field 'name': rejected value [al]; codes [Size.person.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.name,name]; arguments []; default message [name],10,3]; default message [размер должен находиться в диапазоне от 3 до 10]] ",
    "errors": [
        "размер должен находиться в диапазоне от 3 до 10"
    ]
}

Тут пошел в ход список ошибок, который мы добавили в ApiError. Мы его заполняем в переопределенном обработчике исключения:

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                              HttpHeaders headers, HttpStatus status, WebRequest request) {
    List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(x -> x.getDefaultMessage())
            .collect(Collectors.toList());

    ApiError apiError = new ApiError("Method Argument Not Valid", ex.getMessage(), errors);
    return new ResponseEntity<>(apiError, status);
}

Вообще говоря, стандартный ответ, выдаваемый BasicErrorController, тоже будет содержать этот список ошибок по полям, если в application.properties включить свойство:

server.error.include-binding-errors=always

В этом случае (при отсутствии нашего RestExceptionHandler  с @ControlleAdvice) ответ будет таким:

{
    "timestamp": "2021-03-01T17:15:37.134+00:00",
    "status": 400,
    "error": "Bad Request",
    "message": "Validation failed for object='person'. Error count: 1",
    "errors": [
        {
            "codes": [
                "Size.person.name",
                "Size.name",
                "Size.java.lang.String",
                "Size"
            ],
            "arguments": [
                {
                    "codes": [
                        "person.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                },
                10,
                3
            ],
            "defaultMessage": "размер должен находиться в диапазоне от 3 до 10",
            "objectName": "person",
            "field": "name",
            "rejectedValue": "al",
            "bindingFailure": false,
            "code": "Size"
        }
    ],
    "path": "/persons/"
}

Мы просто сократили информацию.

MethodArgumentTypeMismatchException

Полезно знать еще исключение MethodArgumentTypeMismatchException, оно возникает, если тип аргумента неверный. Например, наш метод контроллера получает Person по id:

@GetMapping(value = "/{personId}")
   public Person getPerson(@PathVariable("personId") Long personId) throws EntityNotFoundException {
       return personRepository.getOne(personId);
   }

А мы передаем не целое, а строковое значение id:

GET http://localhost:8080/persons/mn

Тут то и возникает исключение MethodArgumentTypeMismatchException. Давайте его обработаем:

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
protected ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex,HttpStatus status,
                                                                  WebRequest request) {
    ApiError apiError = new ApiError();
    apiError.setMessage(String.format("The parameter '%s' of value '%s' could not be converted to type '%s'",
            ex.getName(), ex.getValue(), ex.getRequiredType().getSimpleName()));
    apiError.setDebugMessage(ex.getMessage());
    return new ResponseEntity<>(apiError, status);
}

Проверим ответ сервера (код ответа будет 400):

{
    "message": "The parameter 'personId' of value 'mn' could not be converted to type 'long'",
    "debugMessage": "Failed to convert value of type 'java.lang.String' to required type 'long'; nested exception is java.lang.NumberFormatException: For input string: "mn""
}

NoHandlerFoundException

Еще одно полезное исключение — NoHandlerFoundException. Оно возникает, если на данный запрос не найдено обработчика.

Например, сделаем запрос:

GET http://localhost:8080/pers

По данному адресу у нас нет контроллера, так что возникнет NoHandlerFoundException.  Добавим обработку исключения:

@Override
protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers,
                                                               HttpStatus status, WebRequest request) {
    return new ResponseEntity<Object>(new ApiError("No Handler Found", ex.getMessage()), status);
}

Только учтите, для того, чтобы исключение выбрасывалось, надо задать свойства в файле application.properties:

spring.mvc.throw-exception-if-no-handler-found=true
spring.web.resources.add-mappings=false

Проверим ответ сервера (код ответа 404):

{
    "message": "No Handler Found",
    "debugMessage": "No handler found for GET /pers"
}

Если же не выбрасывать NoHandlerFoundException и не пользоваться нашим обработчиком, то ответ от BasicErrorController довольно непонятный, хотя код  тоже 404:

{
    "timestamp": "2021-03-01T17:35:59.204+00:00",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/pers"
}

Обработчик по умолчанию

Этот обработчик будет ловить исключения, не пойманные предыдущими обработчиками:

@ExceptionHandler(Exception.class)
    protected ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
        ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, "prosto exception", ex);
        return new ResponseEntity<>(apiError, HttpStatus.INTERNAL_SERVER_ERROR);
    }

Заключение

Мы рассмотрели:

  • как сделать обработку исключений в едином классе, аннотированном @ControllerAdvice;
  • как переопределить формат  JSON-ответа, выдаваемого при возникновении исключения;
  • как воспользоваться классом-заготовкой ResponseEntityExceptionHandler и переопределить его обработчики так, чтобы тело ответов не было пустым;

Обратите внимание, что все не переопределенные методы ResponseEntityExceptionHandler будут выдавать пустое тело ответа.

Код примера доступен на GitHub.

So you’re developing a REST API with Spring Boot and now you need to know how to handle errors.

Good news: I’ll show you how to do that in this guide.

This is normally the point where I say «It’s easier than you think,» but truth be told if you want to handle errors properly, it’s going to require some up-front planning and extra development.

But once you’ve got the framework in place, you can start handling different errors with ease.

Let’s get started.

When a Status Code Isn’t Enough

After reading those last few sentences you might be thinking to yourself: «Well if it takes that much extra work, I’ll probably just send back an HTTP status code that describes the problem.»

And you certainly could go that route. But is that really enough?

Let’s say your REST API got a request that’s unauthorized. In that case, you’d just send back a 401 (Unauthorized) and leave it at that.

But that status code doesn’t explain why the request is unauthorized.

Were the credentials invalid? Was the JSON token missing? Was it expired? Did it have an invalid signature?

There are plenty of reasons why a request could be considered unauthorized. If your application just sends back a 401 with no explanation that doesn’t offer a whole lot of helpful advice.

Similarly, an invalid request (400) status code by itself just tells the client that the request was invalid. But it doesn’t say why.

In short: you need to send back more than just a status code.

On the Importance of a Response Status

When it comes to sending a response from your API, it’s a great idea to not just send the response itself, but to send a response status along with it.

Why? See above. You need to give your clients as much info as possible. That’s how the user will know how to take corrective action if there’s a problem.

And please note: the response status is not the same as the HTTP status code.

The response status includes a machine-readable code (like «SUCCESS») and some human-readable text (like «Registration successful!»).

The HTTP status code is a 3-digit number like 200 (OK), 400 (Bad Request), or 500 (Internal Server Error).

And yes, you need both the response status and the HTTP status code.

Here’s why: in the event of an unauthorized action (HTTP status code 401), you can send back a response status with a code (like «BAD_CREDENTIALS») and some explanatory text (like «Invalid username or password»).

So to emphasize the recurring theme here: the HTTP status code by itself isn’t good enough.

Now let’s take a look at some examples. Here’s an example of a successful response:

{
    "responseStatus": {
        "statusCode": "OK",
        "message": "Successfully registered!"
    },
    "response": {
        "id": "6164c4391160726f07cc3828",
        "username": "george",
        "firstName": "brian",
        "lastName": "carey",
        "email": "you@toohottohandle.com",
        "phoneNumber": "919-555-1212",
     }
}

And here’s an example of an error response:

{
    "responseStatus": {
        "statusCode": "ERROR",
        "message": "Validation error"
    },
    "response": [
        {
            "field": "lastName",
            "defaultMessage": "must not be null"
        }
    ]
}

Okay. Now that you know the requirements, it’s time to start putting some code together.

The Use Case

The use case for this API (and this guide) is to create an endpoint that handles new user registration. 

Users who register will need to provide info in mandatory fields: first name, last name, username, phone number, password, and email address.

If the user doesn’t provide valid info, the API sends back an error response with an HTTP status of 400 (Bad Request). The response body will explain which fields are invalid and why.

Here’s the model class that represents the registration form:

public class Registrant {

    @NotNull
    @Size(min=1, max=32, message="First name must be between 1 and 32 characters")
    private String firstName;
    
    @NotNull
    @Size(min=1, max=32, message="Last name must be between 1 and 32 characters")
    private String lastName;
    
    @NotNull
    @Size(min=5, max=12, message="Username must be between 5 and 12 characters")
    private String username;
    
    @NotNull
    @Size(min=8, max=20, message="Password must be between 8 and 20 characters")
    private String password;
    
    @NotNull
    @Pattern(regexp = "^[_A-Za-z0-9-\+]+(\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\.[A-Za-z0-9]+)*(\.[A-Za-z]{2,})$", message="Email address is invalid")
    private String emailAddress;

    @NotNull
    private String phone;

//getters and setters

}

The annotations handle the validations. If you’re unfamiliar with how all of that works, feel free to read all about validation in Spring Boot and then come on back here.

Now that you’ve got the model in place, it’s time to write the code that handles responses.

ResponseStatusCode

First up is ResponseStatusCode. It’s an enum that looks like this:

public enum ResponseStatusCode {
    OK, ERROR, UNAUTHORIZED;
}

Only three (3) status codes to keep things simple. But you’re free to use as many as you want.

You can also create various types of status codes. You might use numerical codes, for example. Or you might name the status codes after the members of your favorite K-Pop band.

It’s up to you.

The important thing is: you need to let the client developers know what each status code means so they can act accordingly.

ResponseStatus

The ResponseStatus class marries a status code to a message.

public class ResponseStatus {
	
    private ResponseStatusCode statusCode;
    private String message;
	
    public ResponseStatusCode getStatusCode() {
        return statusCode;
    }
    public void setStatusCode(ResponseStatusCode statusCode) {
        this.statusCode = statusCode;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}

That’s pretty straightforward. And it represents the «responseStatus» field you saw in the example above.

RestResponse

Now you need to create a class that includes the three (3) important parts of every response:

  • The response status
  • The response itself
  • The HTTP status code

Remember: the response status is not the same as the HTTP status code. See above.

Start by creating an interface that defines the methods returning those three (3) pieces of info:

public interface IRestResponse<T> {

    ResponseStatus getResponseStatus();
    T getResponse();
    int getHttpStatusCode(); 
    
}

There ain’t nothing too complicated about that.

But note that it’s using a generic (the <T> next to the interface name). What’s that all about?

Go back to the two sample responses above and take a look at the «response» fields. You’ll note that they’re different.

And that makes sense because the response could be anything. It might be a list of field errors or it might be details about a recently created user.

Because the response could be anything, it’s a great idea to use a generic when representing it in code so the compiler will complain if you’re inconsistent with your type references.

Now here’s a concrete implementation of that interface:

public class RestResponse<T> implements IRestResponse<T> {

    private ResponseStatus responseStatus;
    private T response;
    
    @JsonIgnore
    private int httpStatusCode;

    @Override
    public int getHttpStatusCode() {
        return httpStatusCode;
    }    
    
    public void setHttpStatusCode(int httpStatusCode) {
        this.httpStatusCode = httpStatusCode;
    }
    
    @Override
    public T getResponse() {
        return response;
    }
    
    public void setResponse(T response) {
        this.response = response;
    }
    
    @Override
    public ResponseStatus getResponseStatus() {
        return responseStatus;
    }
    
    public void setResponseStatus(ResponseStatus responseStatus) {
        this.responseStatus = responseStatus;
    }
    
    public String toString() {
        return ReflectionToStringBuilder.toString(this);
    }
}

I’ve chosen to create just one implementation, but you might choose to do more than one. For example, you might have one implementation for error responses and another for success responses.

By the way: note the @JsonIgnore on top of httpStatusCode. You should include that so that the response body doesn’t show the HTTP status code. It doesn’t need to display the status code because it gets sent back with the response anyway.

ValidationError

The ValidationError class lives up to its name by representing a field validation error like the one you saw in the sample response above.

@JsonInclude(Include.NON_NULL)
public class ValidationError {

    private String field;
    private String code;
    private String defaultMessage;

//getters and setters

}

I’m not using the code property here but you’re welcome to use it if you need it.

The field property holds the name of the field with the validation error and the defaultMessage property holds the explanatory, human-readable text.

ValidationUtil

The ValidationUtil class includes convenience methods related to valdiation. Here’s a method that converts Spring’s BindingResult object to a List of ValidationError objects.

public class ValidationUtil {
   
    public static List<ValidationError> convertBindingResultToValidationErrors(BindingResult bindingResult) {
        List<ValidationError> errors = new ArrayList<>();
        
        if (bindingResult != null) {
            bindingResult.getFieldErrors().forEach(violation -> {
                String message = violation.getDefaultMessage();
                String field = violation.getField();
                
                ValidationError error = new ValidationError();
                //error.setCode(field);
                error.setDefaultMessage(message);
                error.setField(field);
                
                errors.add(error);
            });
        }
        
        return errors;
    }
}

I commented out the line that sets the code because, as I mentioned above, I’m not using that here.

Also note that the method returns an empty List, rather than a null, if there are no errors.

ResponseEntityUtil

The ResponseEntityUtil class includes convenience methods that create ResponseEntity objects. 

If you’re unfamiliar with the ResponseEntity class, it’s part of the Spring framework. I use it as a convenient way to send back an HTTP status with a response at the same time.

Here’s a method in ResponseEntityUtil that creates a basic ResponseEntity with the three (3) important elements of a response:

    public static <T> ResponseEntity<IRestResponse<T>> createResponseEntity(IRestResponse<T> response) {
        return ResponseEntity
                    .status(HttpStatus.valueOf(response.getHttpStatusCode()))
                    .body(response);
    }

Once again: note the use of the generic. That’s important here because the T defines the type of response sent back.

Also keep in mind the ReponseEntity also uses a generic. The type parameter in the code above is IRestResponse.

That makes sense because, in this API, every response will include a response status, an HTTP status code, and the response itself. All three (3) of those pieces of info can be gleaned from an IRestResponse object.

The body of the method constructs the ResponseEntity object with the assistance of the status() static method. That method accepts the HTTP status as an int value.

Finally, the method sets the actual response with the body() method. That’s the part that gets included in the response body.

Here are a couple of convenience methods in ResponseEntityUtil for creating error and success response statuses:

    private static ResponseStatus getErrorResponseStatus(String message) {
        ResponseStatus status = new ResponseStatus();
        status.setMessage(message);
        status.setStatusCode(ResponseStatusCode.ERROR);
        
        return status;
    }
    
    
    private static ResponseStatus getSuccessResponseStatus(String message) {
        ResponseStatus status = new ResponseStatus();
        status.setMessage(message);
        status.setStatusCode(ResponseStatusCode.OK);
        
        return status;
    }

Both of those methods accept a String message that gets sent back with the response status. You saw examples of those messages in the sample responses above.

Here’s a convenience method that sends back a response with validation errors:

    public static ResponseEntity<IRestResponse<List<ValidationError>>> createResponseEntityWithValidationErrors(List<ValidationError> errors) {
        RestResponse<List<ValidationError>> fullResponse = new RestResponse<>();
        
        if (errors != null && errors.size() > 0) {
            ResponseStatus responseStatus = getErrorResponseStatus("Validation error");
            
            fullResponse.setResponse(errors);
            fullResponse.setResponseStatus(responseStatus);
            fullResponse.setHttpStatusCode(HttpStatus.BAD_REQUEST.value());            
        }
        
        return createResponseEntity(fullResponse);
    }

That method starts off by creating a response status with the message «Validation error.»

After that, it populates the RestResponse object with the list of errors, the just-created response status, and an HTTP status code of 400 (Bad Request).

Then it invokes the createResponseEntity() method you already saw to construct the ResponseEntity object.

Here’s an example of a convenience method that returns a successful response:

    public static <T> ResponseEntity<IRestResponse<T>> createSuccessfulResponseEntity(String message, int httpStatusCode, T response) {
        ResponseStatus status = getSuccessResponseStatus(message);
        
        RestResponse<T> fullResponse = new RestResponse<>();
        fullResponse.setResponseStatus(status);
        fullResponse.setHttpStatusCode(httpStatusCode);
        fullResponse.setResponse(response);
        
        return createResponseEntity(fullResponse);
    }

That method accepts the response status message and the HTTP status code in addition to the response itself.

Why? Because a successful HTTP response status code could be a few different options in the 200’s. And the message itself might vary from one success to another.

Ground Controller to Major Tom

Thus far you’ve laid the groundwork, now let’s put it to use.

Here’s a controller method that creates a new user from a registration page in the application:

    @PostMapping("/")
    public ResponseEntity<IRestResponse<User>> createUser(@Valid @RequestBody Registrant registrant, BindingResult bindingResult) {
        LOG.debug("Registrant is " + registrant);
        
        List<ValidationError> validationErrors = ValidationUtil.convertBindingResultToValidationErrors(bindingResult);
        
        //look for any validations not caught by JSR 380
        registrantService.validateRegistrant(registrant, validationErrors);
        
        User savedUser = registrantService.saveUser(registrant);
            
        return ResponseEntityUtil.createSuccessfulResponseEntity("Successfully registered!", HttpStatus.CREATED.value(), savedUser);
    }

That’s a standard method that exposes an endpoint in a Spring Boot REST controller.

First, note that the method is annotated with @PostMapping because, like most requests that persist a new entity, it’s accepting an HTTP POST method.

The method returns a type of ResponseEntity. As I pointed out previously, that’s a great tool to use with Spring controllers.

And, of course, the ResponseEntity type parameter is IRestResponse. The IRestResponse definition the type parameter User because that’s what the client will get back in the event of a successful request.

The method, intuitively named createUser() accepts two parameters: a Registrant object and a BindingResult object.

The @Valid annotation in front of Registrant tells Spring to validate it. The @RequestBody annotation tells Spring that it needs to translate the JSON request body to that Java Registrant class you saw several sections ago.

The BindingResult class is where Spring stores the validation results. That’s all handled by the framework.

Inside the method, the code invokes that static convertBindingResultToValidationErrors() method that translates the BindingResult object to a List of ValidationError objects. You saw that method earlier.

Next, the code invokes the validateRegistrant() method on the RegistrantService object. In the event of any validation errors, that method will throw a runtime exception that Spring will translate to an error response. I’ll show you that in a moment.

In the meantime, you might be wondering why there’s another layer of validation. It’s for a good reason, and the comments above that validateRegistrant() line give the game away.

Those cute little annotations in the Registrant class will catch many validation errors. But they won’t catch all of them.

For example, you want to make sure the user doesn’t enter an email address that’s already in use. That doesn’t get handled with those annotations.

So that second layer of validation exists. Now let’s look at the code:

    public void validateRegistrant(Registrant registrant, List<ValidationError> errors) {
        validateUniqueName(errors, registrant);
        validateUniqueEmail(errors, registrant);
        validateRecaptcha(errors, registrant);
        
        LOG.debug("validation is " + errors);
        
        if (errors.size() > 0) {
            throw new InvalidRegistrantRequestException(errors);
        }
    }

There you see three additional validation checks (unique name, unique email address, and captcha validation).

You don’t need to know about the details of those methods. Just know that any failure in those validation checks results in a new ValidationError object going into the errors List.

After all the checks, the code looks at errors to see if it’s got anything in it (the size is more than 0). If so, it throws an InvalidRegistrantRequestException. Let’s look at that next.

InvalidRegistrantRequestException

InvalidRegistrantRequestException is a runtime exception. That’s why it doesn’t need to be caught in the code you see above.

Here’s what it looks like:

public class InvalidRegistrantRequestException extends RuntimeException {

    private List<ValidationError> errors;
    
    public InvalidRegistrantRequestException(List<ValidationError> errors) {
        super("Registrant validation failed!");
        this.errors = errors;
    }
    
    public List<ValidationError> getErrors() {
        return errors;
    }
}

As you can see, the public constructor accepts a list of validation errors. Those are the errors that get used to create the ResponseEntity object that eventually gets returned to the client.

But how does it get returned? With the aid of an exception handler.

The Exception Handler

The Spring framework makes it easy to handle exceptions and transform them into exactly the types of responses you need to send back to clients. One of the ways it does that is with the assistance of exception handlers.

Back in that same controller class you were looking at earlier, add this method:

    @ExceptionHandler(InvalidRegistrantRequestException.class)
    public ResponseEntity<IRestResponse<List<ValidationError>>> invalidRegistrant(InvalidRegistrantRequestException ex) {
        List<ValidationError> errors = ex.getErrors();
        return ResponseEntityUtil.createResponseEntityWithValidationErrors(errors);
    }

First of all, the way to identify a method that’s an exception handler is with the appropriately named @ExceptionHandler annotation.

But you also have to include the type of exception it handles. You’ll see that in the parentheses.

The method itself is structured like just about any other method in a controller. It accepts various input parameters and returns a ResponseEntity type.

Note that one of the parameters is the exception object itself. Yep, you can do that with an exception handler.

The body of the method uses that exception object to construct an error response using the createResponseEntityWithValidationErrors() method you saw earlier.

So to recap: if there are any validation errors at all, that method above gets called and the client receives an error response.

Back to the Other Part of the Controller

Let’s revisit that other method:

    @PostMapping("/")
    public ResponseEntity<IRestResponse<User>> createUser(@Valid @RequestBody Registrant registrant, BindingResult bindingResult) {
        LOG.debug("Registrant is " + registrant);
        
        List<ValidationError> validationErrors = ValidationUtil.convertBindingResultToValidationErrors(bindingResult);
        
        //look for any validations not caught by JSR 380
        registrantService.validateRegistrant(registrant, validationErrors);
        
        User savedUser = registrantService.saveUser(registrant);
            
        return ResponseEntityUtil.createSuccessfulResponseEntity("Successfully registered!", HttpStatus.CREATED.value(), savedUser);
    }

If registrantService.validateRegistrant() doesn’t throw an exception, the code above will persist the registrant as a User object.

And then it returns that persisted User object in a success response.

There’s the code. Now it’s time to test it out.

The Postman Always Rings Twice

Now fire up the Spring Boot application with all this wonderful new code. Then launch Postman to do some testing.

Start by creating a request that intentionally leaves out the last name. Like this:

{
    "firstName": "brian",
    "password": "allowishes",
    "emailAddress": "you@toohottohandle.com",
    "phone": "919-555-1212",
    "username": "george"
}

POST that to your endpoint and you should get something like this:

{
    "responseStatus": {
        "statusCode": "ERROR",
        "message": "Validation error"
    },
    "response": [
        {
            "field": "lastName",
            "defaultMessage": "must not be null"
        }
    ]
}

In fact, that’s exactly what I’m getting:

Pay attention to the HTTP status code where you see the red arrow. That’s exactly what it should be (400 for Bad Request).

Now make sure the array works. Take out the first name in addition to the last name and run it again.

You’ll get this:

Cool. So the API returns all validation errors at once. That gives the user the opportunity to fix them all at once.

Now try a successful response.

Awesome. It worked.

I should probably clear those nulls out of the response. But that’s something I can take care of later.

Wrapping It Up

This was a fairly lengthy guide. In the event I missed something, you’re more then welcome to look at the code I have on GitHub.

The utility classes/methods are in their own project and included as a dependency in the API. You can find them here.

If you want to see the API code (still a work in progress as of this writing), you can look at it here. 

Then it’s up to you to write some code yourself. Take the patterns that you saw in this article and make them your own.

Have fun!

Photo by Poppy Thomas Hill from Pexels


posted 4 years ago

  • Mark post as helpful


  • send pies

    Number of slices to send:

    Optional ‘thank-you’ note:



  • Quote
  • Report post to moderator

I got an error as above «Error 400: SRVE0295E: Error reported: 400»

Image model:

package com.avaya.ocp.automation.common.client.email;

import com.fasterxml.jackson.annotation.JsonProperty;

public class Images {

   @JsonProperty(«name»)

   private String  name;

   @JsonProperty(«type»)

   private String type;

   @JsonProperty(«size»)

   private String size;

   @JsonProperty(«attachment»)

   private String attachment;

   @JsonProperty(«guid»)

   private String guid;

   public Images(String name, String type, String size, String attachment, String guid) {

       this.name = name;

       this.type = type;

       this.size = size;

       this.attachment = attachment;

       this.guid = guid;

   }

   public Images() {

   }

   public String getName() {

       return name;

   }

   public void setName(String name) {

       this.name = name;

   }

   public String getType() {

       return type;

   }

   public void setType(String type) {

       this.type = type;

   }

   public String getSize() {

       return size;

   }

   public void setSize(String size) {

       this.size = size;

   }

   public String getAttachment() {

       return attachment;

   }

   public void setAttachment(String attachment) {

       this.attachment = attachment;

   }

   public String getGuid() {

       return guid;

   }

   public void setGuid(String guid) {

       this.guid = guid;

   }

   @Override

   public String toString() {

       return «Images{» +

               «name='» + name + »’ +

               «, type='» + type + »’ +

               «, size='» + size + »’ +

               «, attachment=» + attachment +

               «, guid='» + guid + »’ +

               ‘}’;

   }

}

Code in java

String pathfile = «D:\1.Work\1.Documnent\input.txt»;

String encoded = encodeFileToBase64Binary(pathfile);

encoded = «data:image/png;base64,»+ encoded;

Random random = new Random();

int a = random.nextInt(100);

String guid = «aec0f9ee-08f7-4e69-b3a0-60460e2c5f»+a;

String datahash =»»B74DE93A5DAEE68AEE7295CEDBADD86337B819D1ED39F333F0CA286C193359972B81EEFB1DD19A7B4E83CAF4C3D66767FDD83B7CC9875ADE7C3CEDAEEAF»»;

Images images = new Images();

images.setName(«filename»);

images.setType(«type»);

images.setSize(«size»);

images.setAttachment(«encoded»);

images.setGuid(«guid»);

dataServicesClient.upLoadSignature(images);

System.out.println(«Hello mmmmmmmmm «+ images);

dataServicesClient as below:

@POST

@Path(«/signatures/images»)

@Consumes(MediaType.APPLICATION_JSON)

public TextNode upLoadSignature(Images images);

===> The request can run successfull by POSTMAN

Capture.PNG

[Thumbnail for Capture.PNG]

Run by postman

Понравилась статья? Поделить с друзьями:
  • Spiffs error esptool not found ошибка
  • Splinter cell double agent как изменить разрешение на 1920 1080
  • Spn 523470 fmi 0 ошибка камаз 6520
  • Splinter cell double agent runtime error
  • Spn 523470 fmi 0 камаз код ошибки