Handle SpringBoot ResponseBodyAdvice Error: xxx cannot be cast to java.lang.String

Handle SpringBoot ResponseBodyAdvice Error: xxx cannot be cast to java.lang.String

·

4 min read

I encountered an error when trying to use the ResponseBodyAdvice interface to customize the response after executing the controller method with @ResponseBody annotation. The issue occurred when a controller method was returning a String, resulting in the error 'org.example.MyHttpResponse cannot be cast to java.lang.String'.

The MyHttpResponse is a custom entity used to return globally formatted data types.

1. Reproduce the Error

First, let’s see the code
MyTestController:

@RestController
@RequestMapping("/api/v1")
public class MyTestController {

    @GetMapping("/")
    public String hello() {
        // Call service to get result
        return "Hello, World!";
    }
}

GlobalResponseBodyAdvice:

@RestControllerAdvice(basePackages = "org.example.controller")
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {

    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public MyHttpResponse<Object> globalExceptionHandler(Exception exception) {
        System.out.println("System Internal Error: " + exception.getMessage());
        return MyHttpResponse.fail(HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 4xx error handling
        if(body instanceof MyHttpResponse) {
            return body;
        }

        // controller handling
        if(body instanceof List) {
            return MyHttpResponse.success((List<Object>) body);
        }

        return MyHttpResponse.success(body);
    }
}

MyHttpResponse:

public class MyHttpResponse<T> {
    private Integer status;

    private String message;

    private List<T> data;

    public MyHttpResponse() {
    }

    public MyHttpResponse(Integer status, String message, List<T> data) {
        this.status = status;
        this.message = message;
        this.data = data;
    }

    /**
     * success response
     * @param data data
     * @return response
     * @param <U> data type
     */
    public static <U> MyHttpResponse<U> success(List<U> data) {
        return new MyHttpResponse<>(
                HttpStatus.OK.value(),
                HttpStatus.OK.getReasonPhrase(),
                data
        );
    }

    public static <U> MyHttpResponse<U> success(U data) {
        return new MyHttpResponse<>(
                HttpStatus.OK.value(),
                HttpStatus.OK.getReasonPhrase(),
                Collections.singletonList(data)
        );
    }

    public static MyHttpResponse<Object> success() {
        return new MyHttpResponse<>(
                HttpStatus.OK.value(),
                HttpStatus.OK.getReasonPhrase(),
                Collections.emptyList()
        );
    }

    /**
     * fail response
     * @param status fail details
     * @return response
     */
    public static MyHttpResponse<Object> fail(HttpStatus status) {
        return new MyHttpResponse<>(
                status.value(),
                status.getReasonPhrase(),
                Collections.emptyList()
        );
    }

    // getter, setter...
}

When we send a request curl localhost:8080/api/v1/ , we can see the result: {"status": 500, "message": "Internal Server Error", "data":[]} Checking the log:

System Internal Error: org.example.entity.MyHttpResponse cannot be cast to java.lang.String

It is obvious that somehow we got an exception and the exception is captured by the ExceptionHandler in GlobalResponseAdvice.

2. Analyze the Error

We know that when requests like localhost:8080/api/v1/come in, Spring uses DispatcherServlet to handle them.

DispatcherServlet Inheritence Tree

Checking source code doDispatch():

source code

Now we know that the error occurs because we change the type of the value from java.lang.String to org.example.entity.MyHttpResponse in GlobalResponseAdvice.

3. Handle the Error

There are two ways to sort this out:

  1. Perform specific processing on String in GlobalResponseAdvice beforeBodyWrite() method:

     @Override
     @SuppressWarnings("unchecked")
     public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
         // 4xx error handling
         if(body instanceof MyHttpResponse) {
             return body;
         }
    
         // controller handling
         if(body instanceof List) {
             return MyHttpResponse.success((List<Object>) body);
         }
         // handle string to avoid type change.
         if(body instanceof String) {
             try {
                 return new ObjectMapper().writeValueAsString(MyHttpResponse.success(body));
             } catch (JsonProcessingException e) {
                 throw new RuntimeException(e);
             }
         }
    
         return MyHttpResponse.success(body);
     }
    
  2. Add custom converter(Recommended):

     @Component
     @Order(Ordered.HIGHEST_PRECEDENCE)
     public class CustomStringHttpMessageConverter extends AbstractHttpMessageConverter<MyHttpResponse<String>> {
    
         private final ObjectMapper objectMapper;
    
         public CustomStringHttpMessageConverter(ObjectMapper objectMapper) {
             this.objectMapper = objectMapper;
         }
    
         @Override
         public List<MediaType> getSupportedMediaTypes() {
             return Collections.singletonList(MediaType.APPLICATION_JSON);
         }
    
         @Override
         protected boolean supports(Class<?> clazz) {
             return String.class == clazz;
         }
    
         @Override
         protected MyHttpResponse<String> readInternal(Class<? extends MyHttpResponse<String>> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
             throw new UnsupportedOperationException("This converter can only support writing operation.");
         }
    
         @Override
         protected void writeInternal(MyHttpResponse<String> s, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
             String str = this.objectMapper.writeValueAsString(s);
             StreamUtils.copy(str.getBytes(StandardCharsets.UTF_8), outputMessage.getBody());
         }
     }
    

4. Bonus Part: What exactly do these converters do?

Here’s a description on the official site:

Spring MVC uses the HttpMessageConverter interface to convert HTTP requests and responses. Sensible defaults are included out of the box. For example, objects can be automatically converted to JSON (by using the Jackson library) or XML (by using the Jackson XML extension, if available, or by using JAXB if the Jackson XML extension is not available). By default, strings are encoded in UTF-8.

😁And here’s ChatGPT’s answer with a question Can you play as an export of SpringBoot and tell me what does HttpMessageConverter do?

In Spring Boot, the HttpMessageConverter interface is responsible for converting HTTP requests and responses to and from Java objects. It facilitates the conversion of HTTP payloads (such as JSON, XML, or other data formats) into corresponding Java objects and vice versa.

HttpMessageConverters are employed by Spring Boot’s web component to handle the process of serializing Java objects into an HTTP response body or deserializing an HTTP request body into Java objects.

Spring Boot comes with a variety of built-in HttpMessageConverters that support different content types such as JSON, XML, form data, and more. These converters are automatically configured and selected based on the request’s Content-Type and the supported media types defined in the application.

Additionally, developers can also define custom HttpMessageConverters by implementing the HttpMessageConverter interface. This allows for handling specific content types or customization of the conversion process.

Overall, the HttpMessageConverters in Spring Boot play a crucial role in enabling the seamless exchange of data between Java objects and the outside world via HTTP requests and responses.

You can also find this blog on my Medium blog: