“Ideas of optimizing interface design” series: Part 5 – How to handle exceptions in the interface in a unified manner

Series article navigation
Part 1 – Some twists and turns of interface parameters
Part 2-Design and Implementation of Interface User Context
Part 3 – Leaving traces of the user calling the interface
Part 4-Permission Control of Interfaces
Part 5 – How to handle exceptions in the interface in a unified manner
Part 6 – Some methods of interface anti-shaking (anti-re-submission)
Part 7-Interface current limiting strategy

This article refers to the project source code address:summo-springboot-interface-demo

Foreword

Hello everyone! I am Sum Mo, a front-line low-level code farmer. I usually like to study and think about some technology-related issues and compile them into articles. This is limited to my own level. If there are any inappropriate expressions in the article or code, please feel free to enlighten me.

As a veteran coder who has been working for six years, my job is mainly to develop back-end Java business systems, including various management backends and applets. In these projects, I have designed single/multi-tenant systems, interfaced with many open platforms, and worked on more complex applications such as message centers. But fortunately, I have not yet encountered an online system due to code A crash resulting in capital losses. There are three reasons for this: first, the business system itself is not complicated; second, I have always followed the code specifications of a major manufacturer and wrote code according to the specifications as much as possible during the development process; third, after years of accumulation of development experience, I have become A journeyman who has mastered some practical skills.

BUG is really not unfamiliar to programmers. When a BUG occurs in the code, exceptions will also appear. However, BUG does not equal an exception. BUG is just a cause of exceptions. There are many reasons for exceptions. In this article, I will only talk about how to handle interface-related exceptions.

1. Classification of interface exceptions

In interface design, you should try to avoid using exceptions to control the flow. The interface should return clear error codes and error messages as much as possible instead of throwing exceptions directly.

1. Business Exception

This is a business logic error that may occur during interface processing, such as parameter verification failure, insufficient permissions, etc. These exceptions are usually expected, and corresponding error codes and error messages can be provided to the caller.

2. System Exception

This is an unexpected error that may occur during interface processing, such as database exception, network exception, etc. These exceptions are usually unknown and may cause the interface to not respond properly. This kind of error not only needs to record exception information and notify the system administrator to handle it, but also needs to be encapsulated and prompted, and the error cannot be directly returned to the user.

3. Client Exception

This is an error that may occur when the caller uses the interface, such as request parameter errors, request timeout, etc. These exceptions are usually caused by caller errors and there is no problem with the interface itself. You can choose whether to return error information to the caller according to the specific situation.

2. Common solutions to interface exceptions

1. Exception catching and handling

In the implementation code of the interface, you can use the try-catch statement to catch exceptions and handle them accordingly. You can choose to convert the exception into an appropriate error code and error message, and then return it to the caller. Or you can choose whether to record exception logs based on specific circumstances, and notify the system administrator for processing.

2. Unified exception handler

A unified exception handler can be used to uniformly handle interface exceptions. In Spring Boot, you can use the @ControllerAdvice and @ExceptionHandler annotations to define a global exception handler. In this way, exceptions thrown by all interfaces can be processed uniformly, for example, converted into specific error codes and error messages, and returned to the caller.

3. Throw a custom exception

You can define some custom exception classes based on business needs, inherit RuntimeException or other appropriate exception classes, and throw these exceptions in the interface. In this way, when an exception occurs, an exception can be thrown directly, and the upper-layer caller can catch and handle it.

4. Return error code and error message

You can define a set of error code and error message specifications in the interface. When an exception occurs, the corresponding error code and error message are returned to the caller. In this way, the caller can perform corresponding processing according to the error code, such as displaying the error message to the user or performing corresponding logical processing.
For example, a pop-up prompt like this

5. Jump to the specified error page

For example, when encountering errors such as 401, 404, 500, etc., the SpringBoot framework will return its own error page. Here we can actually rewrite some more beautiful and friendly error prompt pages ourselves, and it is best to guide the user back to the correct page. The operation is as follows:

Instead of the following

3. Unified handling of interface exceptions

From the previous two paragraphs, we can find that there are many reasons for exceptions, there are many places where exceptions occur, and there are many ways to deal with exceptions. Based on the above three situations, we need a place to uniformly receive exceptions and handle exceptions uniformly. As mentioned above, SpringBoot’s @ControllerAdvice annotation serves as a global exception handler to uniformly handle exceptions. But the @ControllerAdvice annotation is not omnipotent. It has a problem:

For the @ControllerAdvice annotation, it is mainly used to handle exceptions in the Controller layer, that is, exceptions that occur in controller methods. Because it is based on the exception handling mechanism of Spring MVC’s controller layer.
The Filter layer is a layer of filters located before the controller, which can be used to pre-process and post-process requests. When the request enters the Filter, it has not yet entered the Controller layer, so the @ControllerAdvice annotation cannot directly handle exceptions in the Filter layer.
So for exceptions in Filter, we need to handle them separately.

1. Use of @ControllerAdvice global exception handler

(1) Custom business exception

Since the SpringBoot framework does not define business-related error codes, we need to customize business error codes. The error codes can be classified according to the complexity of the business, and each error code corresponds to a specific abnormal situation. In this way, when the front-end and back-end handle exceptions uniformly, specific processing logic can be carried out based on the error code, improving the accuracy and efficiency of exception handling. At the same time, defining error codes can also facilitate abnormal monitoring and logging, making it easier to troubleshoot and repair problems.

a. Define common exception status codes

ResponseCodeEnum.java

package com.summo.demo.model.response;


public enum ResponseCodeEnum {<!-- -->
    /**
     * Request successful
     */
    SUCCESS("0000", ErrorLevels.DEFAULT, ErrorTypes.SYSTEM, "Request successful"),
    /**
     * Login related exceptions
     */
    LOGIN_USER_INFO_CHECK("LOGIN-0001", ErrorLevels.INFO, ErrorTypes.BIZ, "User information error"),
    /**
     *Permission related exceptions
     */
    NO_PERMISSIONS("PERM-0001", ErrorLevels.INFO, ErrorTypes.BIZ, "User does not have permission"),
    /**
     * Business related exceptions
     */
    BIZ_CHECK_FAIL("BIZ-0001", ErrorLevels.INFO, ErrorTypes.BIZ, "Business check exception"),
    BIZ_STATUS_ILLEGAL("BIZ-0002", ErrorLevels.INFO, ErrorTypes.BIZ, "Illegal business status"),
    BIZ_QUERY_EMPTY("BIZ-0003", ErrorLevels.INFO, ErrorTypes.BIZ, "Query information is empty"),
    /**
     * system error
     */
    SYSTEM_EXCEPTION("SYS-0001", ErrorLevels.ERROR, ErrorTypes.SYSTEM, "A system error occurred, please try again later"),
    ;

    /**
     * Enumeration encoding
     */
    private final String code;

    /**
     * Error level
     */
    private final String errorLevel;

    /**
     * Error type
     */
    private final String errorType;

    /**
     * Description
     */
    private final String description;

    ResponseCodeEnum(String code, String errorLevel, String errorType, String description) {<!-- -->
        this.code = code;
        this.errorLevel = errorLevel;
        this.errorType = errorType;
        this.description = description;
    }

    public String getCode() {<!-- -->
        return code;
    }

    public String getErrorLevel() {<!-- -->
        return errorLevel;
    }

    public String getErrorType() {<!-- -->
        return errorType;
    }

    public String getDescription() {<!-- -->
        return description;
    }


    public static ResponseCodeEnum getByCode(Integer code) {<!-- -->
        for (ResponseCodeEnum value : values()) {<!-- -->
            if (value.getCode().equals(code)) {<!-- -->
                return value;
            }
        }
        return SYSTEM_EXCEPTION;
    }

}

b. Customized business exception class

BizException.java

package com.summo.demo.exception.biz;

import com.summo.demo.model.response.ResponseCodeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class BizException extends RuntimeException {<!-- -->

    /**
     * error code
     */
    private ResponseCodeEnum errorCode;

    /**
     * Custom error message
     */
    private String errorMsg;

}

(2) Global exception handler

BizGlobalExceptionHandler

package com.summo.demo.exception.handler;

import javax.servlet.http.HttpServletResponse;

import com.summo.demo.exception.biz.BizException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.ModelAndView;

@RestControllerAdvice(basePackages = {<!-- -->"com.summo.demo.controller", "com.summo.demo.service"})
public class BizGlobalExceptionHandler {<!-- -->

    @ExceptionHandler(BizException.class)
    public ModelAndView handler(BizException ex, HttpServletResponse response) {<!-- -->
        ModelAndView modelAndView = new ModelAndView();
        switch (ex.getErrorCode()) {<!-- -->
            case LOGIN_USER_INFO_CHECK:
                //Redirect to login page
                modelAndView.setViewName("redirect:/login");
                break;
            case NO_PERMISSIONS:
                //Set error message and error code
                modelAndView.addObject("errorMsg", ex.getErrorMsg());
                modelAndView.addObject("errorCode", ex.getErrorCode().getCode());
                modelAndView.setViewName("403");
                break;
            case BIZ_CHECK_FAIL:
            case BIZ_STATUS_ILLEGAL:
            case BIZ_QUERY_EMPTY:
            case SYSTEM_EXCEPTION:
            default:
                //Set error message and error code
                modelAndView.addObject("errorMsg", ex.getErrorMsg());
                modelAndView.addObject("errorCode", ex.getErrorCode().getCode());
                modelAndView.setViewName("error");
        }
        return modelAndView;
    }
}

(3) Test effect

@RestControllerAdvice and @ExceptionHandler are very simple to use, let’s test it below (Since it would be too ugly not to write screenshots of the interface, I asked ChatGPT to help me write a simple interface).

a. Common business exception capture
Step one, open the login page

Access link: http://localhost:8080/login
Enter your account number and password, and click Login to enter the home page.

The second step, log in to the home page

The third step is to call an interface that will report an error

Before starting the service, I wrote a method to query the user based on the user name. If the user cannot be queried, I will throw an exception. The code is as follows:

public ResponseEntity<String> query(String userName) {<!-- -->
  //Query users by name
  List<UserDO> list = userRepository.list(
  new QueryWrapper<UserDO>().lambda().like(UserDO::getUserName, userName));
  if (CollectionUtils.isEmpty(list)) {<!-- -->
    throw new BizException(ResponseCodeEnum.BIZ_QUERY_EMPTY, "Querying the user based on the user name is empty!");
  }
  //return data
  return ResponseEntity.ok(JSONObject.toJSONString(list));
}

At this time, we query a user that does not exist
Access interface: http://localhost:8080/user/query?userName=sss
Because there is no user named sss in the database, an exception will be thrown.

b. 403 Insufficient Permission Exception Capture
Step one, open the login page

Access link: http://localhost:8080/login
Log in using Xiao B’s account on the login interface.

The second step, log in to the home page

The third step is to call the interface to delete the user

Calling interface: http://localhost:8080/user/delete?userId=2
Since Little B’s account only has query permission and no deletion permission, a 403 error page is returned.

Note: Before debugging, you need to add a configuration to the application.yml or application.properties configuration file: server.error.whitelabel.enabled=false
This configuration means whether to enable the default error page. Here we have written a set of error pages ourselves, so there is no need for the framework’s own configuration.

2. Exception handling in custom Filter

Since the @ControllerAdvice annotation cannot capture exceptions thrown in custom Filters, here we need to use another method to handle them: the ErrorController interface.

(1) Principle explanation

Spring Boot’s ErrorController is an interface for defining custom logic for handling errors that occur in your application. It allows developers to handle and respond to exceptions in a more flexible way instead of relying on the default error handling mechanism. :

  • Customized error pages: By implementing the ErrorController interface, you can customize your application’s error pages to provide a better user experience. Different error pages or error messages can be provided based on different exception types and HTTP status codes.
  • Error logging: ErrorController can be used to capture and log exceptions in the application and log them. This is very helpful for problem tracking and troubleshooting to understand the details of errors and exceptions occurring in the application.
  • Redirect or forward requests: With ErrorController, requests can be redirected to different URLs or forwarded to other controller methods based on the type of error or other conditions. This is useful for handling errors differently, such as redirecting to a custom error page or executing specific error handling logic.

(2) How to use

Just look at my code to find out how to use it.
CustomErrorController.java

package com.summo.demo.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class CustomErrorController implements ErrorController {<!-- -->

    @RequestMapping("/error")
    public ModelAndView handleError(HttpServletRequest request, HttpServletResponse response) {<!-- -->
        //Get the status code returned by the current response
        int statusCode = response.getStatus();
        //If statusCode exists in the response header, this statusCode will be used by default.
        if (StringUtils.isNotBlank(response.getHeader("statusCode"))) {<!-- -->
            statusCode = Integer.valueOf(response.getHeader("statusCode"));
        }
        if (statusCode == HttpServletResponse.SC_FOUND) {<!-- -->
            // Get the value of the Location response header and redirect it
            String redirectLocation = response.getHeader("Location");
            return new ModelAndView("redirect:" + redirectLocation);
        } else if (statusCode == HttpServletResponse.SC_UNAUTHORIZED) {<!-- -->
            //Redirect to login page
            return new ModelAndView("redirect:/login");
        } else if (statusCode == HttpServletResponse.SC_FORBIDDEN) {<!-- -->
            //Return to 403 page
            return new ModelAndView("403");
        } else if (statusCode == HttpServletResponse.SC_NOT_FOUND) {<!-- -->
            //Return to 404 page
            return new ModelAndView("404");
        } else if (statusCode == HttpServletResponse.SC_INTERNAL_SERVER_ERROR) {<!-- -->
            // Return to page 500 and pass errorMsg and errorCode to the template
            ModelAndView modelAndView = new ModelAndView("500");
            modelAndView.addObject("errorMsg", response.getHeader("errorMsg"));
            modelAndView.addObject("errorCode", response.getHeader("errorCode"));
            return modelAndView;
        } else {<!-- -->
            //Return to other error pages
            return new ModelAndView("error");
        }
    }

}

Careful readers may see that statusCode comes from two places, the first is response.getStatus(); the second is response.getHeader(“statusCode”). The difference between the two is that the first one is automatically set by the framework, and the second one is set by me based on business logic.
The reason is that once an exception is thrown in WebFilter, response.getStatus() will definitely be 500, even if the exception is caused by the user’s identity being invalid. But the exception had to be thrown, so I set the error code by customizing the header of the response and passed it to the /error interface.

(3) Test effect

a, 404 error page, interface not found
Step one, open the login page

Access link: http://localhost:8080/login
Enter your account number and password, and click Login to enter the home page.

The second step, log in to the home page

Step 3: Visit a page that does not exist

Access link: http://localhost:8080/xxxx
Since the xxxx interface has not been defined, the interface will return 404

b, 401 error, user ID is empty or invalid

What I do here is that if the user identity is empty or invalid, then I will jump to the login page by default.
The test method is to open an incognito interface and enter a link: http://localhost:8080/user/query
Since the token in the cookie does not exist, I directly change the status code to 401 regardless of which link I visit. When the CustomErrorController encounters a 401 error, it will redirect to the login page by default.

4. Optimize the re-login experience under the incognito window

In addition to the ErrorController, the global handling of Filter exceptions can also be implemented through custom interceptors. Just one of these two things is enough. Here I will talk about something more advanced, as an example:
I am calling the interface in a incognito window: http://localhost:8080/user/query?userName=小B
Because there is no token in the cookie of the current window, I will redirect to the login page according to the 401 error handling method.
But there is a problem with this: After logging in again, I enter the homepage instead of calling the user/query interface. I have to find this interface again and re-enter the parameters. Moreover, it would be embarrassing if this was a sharing page. After logging in, I would not know what the other party had shared, and the user experience would be very poor. So is there a way to optimize this problem? The answer is yes, how to do it, keep reading.

1. Get the full path of the current request in WebFilter

The so-called full path is “http://localhost:8080/user/query?userName=小B”. How to get it, you can use my method

/**
   * Get the complete path URL, including parameters
   *
   * @param httpServletRequest
   * @return path URL
*/
private String getRequestURL(HttpServletRequest httpServletRequest) {<!-- -->
  String url = httpServletRequest.getRequestURL().toString();
  String query = httpServletRequest.getQueryString();
  if (query != null) {<!-- -->
    url + = "?" + query;
  }
  return url;
}

2. Set the httpServletResponse header where WebFilter throws a 401 error

as follows

httpServletResponse.setHeader("redirectURL",URLEncoder.encode(getRequestURL(httpServletRequest), "utf-8"));

Because the parameters may be in Chinese, URLEncoder needs to be used to convert them to lower meanings.

3. Obtain this jump link in CustomErrorController

//Redirect to the login page or specified page
 if (StringUtils.isNotBlank(response.getHeader("redirectURL"))) {<!-- -->
  return new ModelAndView("redirect:/login?redirectURL=" + response.getHeader("redirectURL"));
 }

The effect is as follows

You can see that we carry a redirectURL parameter after login

4. Submit the redirectURL parameter together when logging in and submitting

 @PostMapping("/login")
public void userLogin(@RequestParam(required = true) String userName,
        @RequestParam(required = true) String password,
        @RequestParam(required = false) String redirectURL,
        HttpServletRequest httpServletRequest,
        HttpServletResponse httpServletResponse) {<!-- -->
  userService.login(userName, password, redirectURL, httpServletRequest, httpServletResponse);
}

5. Redirect to redirectURL after passing verification

 try {<!-- -->
  //If the jump path is not empty, redirect directly to the jump path
  if (StringUtils.isNotBlank(redirectURL)) {<!-- -->
    httpServletResponse.sendRedirect(redirectURL);
    return;
  }
 //Jump to login page
  httpServletResponse.sendRedirect("/index");
  } catch (IOException e) {<!-- -->
  log.error("An exception occurred during redirection", e);
}

The above is the solution to this problem. For the specific code, you can see my demo:summo-springboot-interface-demo