Are you still returning ApiResult? ✋ duck: No, let's look at the best practices for API error handling ✔️

Posted by Koobi on Tue, 11 Jan 2022 03:47:15 +0100

Share Java NET, Javascript, efficiency, software engineering, programming language and other technical knowledge.

This article GitHub TechShare Included, no advertising, Just For Fun.

Why did you write this article?

I believe many Java developers have used objects like ApiResult to wrap Api return types in projects, which is better than not wrapping anything, but is this really the best way?

About encapsulating ResultBean objects, Xiaofeng is light in his Programmer, why are you so tired There has been a good sharing in the series of articles, but the unified encapsulation of ResultBean is actually a repetitive work. Adhering to the concept of DRY, it is necessary to continue to optimize it.

It is not a best practice to uniformly return ApiResult. We must constantly think about optimization, just like the Rethinking Best Practices advocated by React.

Current situation of ApiResult

Let's first look at a common ApiResult object. The code is as follows:

@Data
public class ApiResult<T> implements Serializable {
    private int code;
    private String message;
    private T data;
}

Benefit: the client can use a unified processing method.

Existing problems:

  1. When ApiResult is returned uniformly, even if it is returned normally, code and message attributes will be brought. In most cases, it is redundant.
  2. The Controller layer code is repeated, the return object is repeatedly defined, the packaging call is repeatedly written, and the cleanliness of the code is reduced.
  3. Unified return of 200 status code is not conducive to request monitoring.
  4. ApiResult is responsible for both Api results and error results, which does not comply with the principle of single responsibility.

If the following code for obtaining list data does not involve the request verification within the business expectation, it is not necessary to package a layer of ApiResult. What is the verification within the business expectation? For example, non members cannot obtain the list, and users need to be reminded to buy members in the business, which is a legal request, At this time, you can still use ApiResult to explicitly return the code to the client.

public ApiResult<List<Data>> demo() {
    return ApiResult.ok(getList());
}

ApiResult should be used according to business scenarios, and it is not necessary to use it for every scenario.

When there are more and more API s, the problem of uniformly returning ApiResult will be enlarged. How to solve these problems? Please keep looking.

Use HTTP status code

Many projects use the normal data model when the API call is successful, and return the corresponding HTTP error code and description information when an error occurs. Let's look at the code in jhipster:

@GetMapping("/authors/{id}")
public ResponseEntity<AuthorDTO> getAuthor(@PathVariable Long id) {
    Optional<AuthorDTO> authorDTO = authorService.findOne(id);
    return ResponseUtil.wrapOrNotFound(authorDTO);
}

Meaning of main HTTP status codes:

  • 1XX – Informational
  • 2XX – Success
  • 3XX – Redirection
  • 4XX – Client Error
  • 5XX – Server Error

If the HTTP status code is adopted, it is no longer necessary to return ApiResult uniformly, but the problem also follows. That is, the error code defined in ApiResult is difficult to correspond to the HTTP error code one by one. It is not enough to have HTTP error code and description information alone. It is also necessary to define a special error model.

API error model

How to define a good API error model depends on the complexity of the business. Let's take a look at how several big companies do it.

Let's start with twitter, which omits irrelevant HTTP output information.

HTTP/1.1 400 Bad Request

{"errors":[{"code":215,"message":"Bad Authentication data."}]}

The error code is used, and the error model is an array, which means that multiple errors may be returned.

Let's look at Facebook's Graph API.

HTTP/1.1 200

{
  "error": {
    "message": "Syntax error \"Field picture specified more than once. This is only possible before version 2.1\" at character 23: id,name,picture,picture",
    "type": "OAuthException",
    "code": 2500,
    "fbtrace_id": "xxxxxxxxxxx"
  }
}

Note that it returns a unified 200 status code, and the error model also contains exception types and trace_id, these two attributes help troubleshoot errors.

Finally, take a look at the error model of giant Microsoft Bing.

HTTP/1.1 200

{
  "SearchResponse": {
    "Version": "2.2",
    "Query": { "SearchTerms": "api error codes" },
    "Errors": [
      {
        "Code": 1001,
        "Message": "Required parameter is missing.",
        "Parameter": "SearchRequest.AppId",
        "HelpUrl": "http\u003a\u002f\u002fmsdn.microsoft.com\u002fen-us\u002flibrary\u002fdd251042.aspx"
      }
    ]
  }
}

It also returns a 200 status code, but you can see that it uses a packaging method similar to ApiResult, and also contains input information, input parameters and help links. Is this how the boss does things?

Sure enough, the design of the API error model is different according to the different business complex programs. Among the three, we refer to the API design of twitter to see what needs attention in the implementation of Spring projects. After all, the complexity of most projects can not reach the level of FB and Bing.

Spring API error model practice

The definition of the error model is very simple. The code is as follows.

ErrorResponse.java

@Data
public class ErrorResponse implements Serializable {
    private ErrorDetail error;
}

ErrorDetail.java

@Data
public class ErrorDetail implements Serializable {
    private int code;
    private String message;
    private String type;
}

A type attribute is added to the error details to help better locate exceptions.

When writing in the Controller layer, you need to return to the normal data model, such as List, VO, DTO, etc.

Exceptions are handled using AOP.

Write a ControllerAdvice class,.

@ControllerAdvice
@ResponseBody
@Slf4j
public class CustomExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public ResponseEntity<ErrorResponse> exceptionHandler(Exception exception) {
        return serverErrorResponse(ApiCode.SYSTEM_EXCEPTION, exception);
    }

    private ResponseEntity<ErrorResponse> serverErrorResponse(ApiCode apiCode, Exception exception) {
        String message = apiCode.getMessage();
        //The server needs to log exceptions
        log.error(message, exception);
        //The server uses message in api code to avoid sending sensitive exception information to the client
        return new ResponseEntity<>(errorResponse(apiCode, ErrorMessageType.API_CODE, exception), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    private ResponseEntity<ErrorResponse> requestErrorResponse(ApiCode apiCode, Exception exception) {
        String message = apiCode.getMessage();
        //The client request error only records the debug log
        if (log.isDebugEnabled()) {
            log.debug(message, exception);
        }
        //The client exception uses the message in the exception
        return new ResponseEntity<>(errorResponse(apiCode, ErrorMessageType.EXCEPTION, exception), HttpStatus.BAD_REQUEST);
    }

    private ErrorResponse errorResponse(ApiCode code, ErrorMessageType messageType, Exception exception) {
        ErrorDetail errorDetail = new ErrorDetail();
        errorDetail.setCode(code.getCode());
        if (messageType.equals(ErrorMessageType.API_CODE) || StrUtil.isBlank(exception.getMessage())) {
            errorDetail.setMessage(code.getMessage());
        } else {
            errorDetail.setMessage(exception.getMessage());
        }
        errorDetail.setType(exception.getClass().getSimpleName());

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setError(errorDetail);
        return errorResponse;
    }

    @ExceptionHandler(value = RequestVerifyException.class)
    public ResponseEntity<ErrorResponse> requestVerifyExceptionHandler(RequestVerifyException e) {
        return requestErrorResponse(ApiCode.PARAMETER_EXCEPTION, e);
    }

}

The above code only puts two exceptionhandlers, one for request validation errors and the other for unknown server errors, corresponding to the HTTP status codes of 400 and 500 respectively. Other exceptions need to be handled specially, and the above public errorResponse method is still used, depending on whether the exception is defined as a request exception or a server-side exception.

At this point, the API can return a "beautiful" error model.

Is that over?

Don't go yet. It's not over yet. If the data model returned under normal and error conditions is different, how should the interface document be defined? If swagger is used, we need to add a global output model for 400 and 500 status codes.

It's still a little hard to implement in the latest version of spring fox. Let's look at some of the code.

@Bean
public Docket createRestApi(TypeResolver typeResolver) {
    //Additional error model
    Docket builder = new Docket(DocumentationType.SWAGGER_2)
            .host(swaggerProperties.getHost())
            .apiInfo(apiInfo(swaggerProperties))
            .additionalModels(typeResolver.resolve(ErrorResponse.class));

    //Add 400 error code output model
    List<Response> responseMessages = new ArrayList<>();
    ResponseBuilder responseBuilder = new ResponseBuilder();
    responseBuilder.code("400").description("");
    if (!StringUtils.isEmpty(globalResponseMessageBody.getModelRef())) {
        responseBuilder.representation(MediaType.APPLICATION_JSON)
            .apply(rep -> rep.model(m -> m.referenceModel(
                re -> re.key(key->key.qualifiedModelName(new QualifiedModelName("com.package.api","ErrorResponse")))
            )));
    }
    responseMessages.add(responseBuilder.build());

    builder.useDefaultResponseMessages(false)
        .globalResponses(HttpMethod.GET, responseMessages)
        .globalResponses(HttpMethod.POST, responseMessages);

    return builder.select().build();
}

The above is only part of the code, mainly because it is necessary to attach the model and specify the output model. In the actual project, the model information should be placed in the configuration and added automatically according to the configuration. If readers are interested in the automatic configuration of swagger, they can have the opportunity to write an article to explain it.

Write at the end

I think it's boring to return a unified ApiResult in each interface. Writing a program should be a creative thing. Constantly thinking about best practices and learning excellent design is a small thing that we encounter almost every day in our work. It is worth improving.

Topics: Java Spring Spring Boot Back-end