Skip to content

Wrong response Content-Type in ExceptionHandler after adding dependency "spring-boot-starter-data-rest" #43143

Description

@ivouchak-sc

Spring Boot Version: 3.3.5.
Response content type changed from application/json to application/xml after adding new dependency org.springframework.boot:spring-boot-starter-data-rest.

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-rest' // <-- a new dependecy has been added
	implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
}

Controller

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/test-user")
    public ResponseEntity<User> getUser() {
        User user = new User("test-user");
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(user);
    }

    @GetMapping("/test-user-error")
    public User getUserException() {
        throw new MyCustomException("my-custom-error");
    }
}

Controller Advice

@RestControllerAdvice
public class AdviceController {

    @ExceptionHandler
    public ResponseEntity<ErrorsResponseDto> handleError(MyCustomException e) {
        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(new ErrorsResponseDto(e.getMessage()));
    }
}

Behaviour with spring-boot-starter-data-rest dependency

curl -v http://127.0.0.1:8080/api/users/test-user

*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
> GET /api/users/test-user HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.9.1
> Accept: */*
>
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Wed, 13 Nov 2024 15:47:38 GMT
<
{"name":"test-user"}* Connection #0 to host 127.0.0.1 left intact

logs

2024-11-13T18:47:38.285+03:00 DEBUG 14812 --- [nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.UserController#getUser()
2024-11-13T18:47:38.302+03:00 DEBUG 14812 --- [nio-8080-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/xml;charset=UTF-8, text/xml;charset=UTF-8, application/*+xml;charset=UTF-8]
2024-11-13T18:47:38.304+03:00 DEBUG 14812 --- [nio-8080-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [User[name=test-user]]

Response content-type is application/json. This is OK

curl -v http://127.0.0.1:8080/api/users/test-user-error

> curl -v http://127.0.0.1:8080/api/users/test-user-error
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
> GET /api/users/test-user-error HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.9.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 400
< Content-Type: application/xml;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Wed, 13 Nov 2024 15:51:22 GMT
< Connection: close
<
<ErrorsResponseDto><error>my-custom-error</error></ErrorsResponseDto>* shutting down connection #0

logs

2024-11-13T18:51:22.447+03:00 DEBUG 14812 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.UserController#getUserException()
2024-11-13T18:51:22.451+03:00 DEBUG 14812 --- [nio-8080-exec-6] .m.m.a.ExceptionHandlerExceptionResolver : Using @ExceptionHandler com.example.demo.AdviceController#handleError(MyCustomException)
2024-11-13T18:51:22.454+03:00 DEBUG 14812 --- [nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/xml;charset=UTF-8', given [*/*] and supported [application/xml;charset=UTF-8, text/xml;charset=UTF-8, application/*+xml;charset=UTF-8, application/json, application/*+json]

Response content-type is application/xml. This is WRONG. The content type should be application/json.

Behaviour without spring-boot-starter-data-rest dependency

Let try to remove dependency org.springframework.boot:spring-boot-starter-data-rest and repeat the last HTTP request again.

curl -v http://127.0.0.1:8080/api/users/test-user-error

*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
> GET /api/users/test-user-error HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.9.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 400
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Wed, 13 Nov 2024 15:56:57 GMT
< Connection: close
<
{"error":"my-custom-error"}* shutting down connection #0

logs

2024-11-13T18:56:57.825+03:00 DEBUG 2720 --- [nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.UserController#getUserException()
2024-11-13T18:56:57.834+03:00 DEBUG 2720 --- [nio-8080-exec-2] .m.m.a.ExceptionHandlerExceptionResolver : Using @ExceptionHandler com.example.demo.AdviceController#handleError(MyCustomException)
2024-11-13T18:56:57.858+03:00 DEBUG 2720 --- [nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/xml;charset=UTF-8, text/xml;charset=UTF-8, application/*+xml;charset=UTF-8]

Response content-type is application/json. This is behaviour should be the same with added spring-boot-starter-data-rest dependency.

After logs analyzing the order of supported content types is differ.

The sample application for reproducing the issue is attached below.

Activity

  1. ivouchak-sc commented on Nov 13, 2024

    @ivouchak-sc
    Author

    The sample application for quick reproducing:
    demo.zip

  2. nosan commented on Nov 13, 2024

    @nosan
    Contributor

    Thanks for the detailed description and sample, @ivouchak-sc

    It does not work with org.springframework.boot:spring-boot-starter-data-rest because RepositoryRestMvcConfigurationadds its HandlerExceptionResolver to the beginning of the list. (See
    RepositoryRestMvcConfiguration.extendHandlerExceptionResolvers(...)). This HandlerExceptionResolver is configured
    with default HttpMessageConverter beans and RepositoryRestMvcConfiguration does not re-order them as Spring Boot
    does in org.springframework.boot.autoconfigure.http.HttpMessageConverters.reorderXmlConvertersToEnd(...).

    Since this HandlerExceptionResolver was added to the beginning of the list and converters were not re-ordered you got a response in XML format.

    At the moment, to fix this issue, you can add the following bean:

    @Bean
    RepositoryRestConfigurer reorderHttpMessageConvertersRepositoryRestConfigurer() {
    	return new RepositoryRestConfigurer() {
    		@Override
    		public void configureExceptionHandlerExceptionResolver(ExceptionHandlerExceptionResolver exceptionResolver) {
    			List<HttpMessageConverter<?>> messageConverters = exceptionResolver.getMessageConverters();
    			reorderXmlConvertersToEnd(messageConverters);
    		}
    	};
    }
    
    private void reorderXmlConvertersToEnd(List<HttpMessageConverter<?>> converters) {
    	List<HttpMessageConverter<?>> xml = new ArrayList<>();
    	for (Iterator<HttpMessageConverter<?>> iterator = converters.iterator(); iterator.hasNext(); ) {
    		HttpMessageConverter<?> converter = iterator.next();
    		if ((converter instanceof AbstractXmlHttpMessageConverter) || (converter instanceof MappingJackson2XmlHttpMessageConverter)) {
    			xml.add(converter);
    			iterator.remove();
    		}
    	}
    	converters.addAll(xml);
    }
    

    Honestly, this isn’t the ideal solution. Maybe someone has a better suggestion.

    The other option is pretty straightforward:

    @Bean
    RepositoryRestConfigurer reorderHttpMessageConvertersRepositoryRestConfigurer() {
    	return new RepositoryRestConfigurer() {
    		@Override
    		public void configureExceptionHandlerExceptionResolver(
    				ExceptionHandlerExceptionResolver exceptionResolver) {
    			exceptionResolver.getMessageConverters().add(0, new MappingJackson2HttpMessageConverter());
    		}
    	};
    }
  3. nosan commented on Nov 13, 2024

    @nosan
    Contributor

    Maybe, Spring Boot could also re-order HttpMessageConverter in SpringBootRepositoryRestConfigurer for Spring Data REST.

  4. removed
    for: team-meetingAn issue we'd like to discuss as a team to make progress
    on Nov 14, 2024
  5. bclozel commented on Nov 15, 2024

    @bclozel
    Member

    Thanks for the analysis @nosan, you're spot on.

    I think this is an unfortunate combination of several valid opinions:

    1. Spring Boot reorder HttpMessageConverter instances to put XML ones last. Spring Boot more or less considers that XML is a bit out of fashion and unless asked explicitly, other converters should be used first.
    2. Spring Data REST contributes an error handler with specific opinions (including some provided by users). It's ordering this handler first. It's also getting HttpMessageConverter from the application context, as ordered by Spring Framework and the application.

    We could consider several options here:

    1. provide your own RepositoryRestConfigurer and reorder converters as you see fit. This is the code snippet shown by @nosan in a comment above
    2. the shorter option somehow works in this case but adds a new JSON converter at the top of the list. This is not the converter that is auto-configured so it's unlikely to honor other auto-configurations and preferences in the app.
    3. Spring Boot reorders the converters for Spring Data REST. Unfortunately, that configuration is very much internal and unpacking this would be brittle. Also, other developers might be relying on this behavior for many years.
    4. Spring Framework changes the default order of converters, putting JSON first. I have created Revisit converters and codecs default setup in HTTP stacks  spring-framework#33894 to consider this for Framework 7.0. A major generation is a good fit for such an change.

    While I understand the lack of consistency here, the fact is that neither the HTTP client nor the application are expressing any hint for the content negotiation. In this case, I would argue that you can hardly expect any specific content-type in that situation.

    Here, the easiest way out in the application would be to set a JSON media type in the @ExceptionHandler if you expect clients to always use JSON. Same goes for REST endpoints.

    I also think that setting a default content type (if the content negotiation comes up with nothing) like the following should work:

    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
    
    	@Override
    	public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    		configurer.defaultContentType(MediaType.APPLICATION_JSON);
    	}
    }

    Unfortunately, the ExceptionHandlerExceptionResolver here does not set the ContentNegotiationManager that's available globally. Maybe doing so would solve this particular problem transparently. Could you maybe create an issue and discuss that point with the team?

    I'm closing this issue since the two most viables options should be explored in other projects.

    Thanks!

  6. added
    status: invalidAn issue that we don't feel is valid
    for: external-projectFor an external project and not something we can fix
    and removed on Nov 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

for: external-projectFor an external project and not something we can fixstatus: invalidAn issue that we don't feel is valid

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions