Error Response body changed after Boot 3 upgrade - spring

I have the following Controller endpoint in my project:
#GetMapping(value = "/{id}")
public FooDto findOne(#PathVariable Long id) {
Foo model = fooService.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return toDto(model);
}
My application retrieved the following response when it couldn't find a Foo with the provided id:
{
"timestamp": "2023-01-06T08:43:12.161+00:00",
"status": 404,
"error": "Not Found",
"path": "/foo/99"
}
However, after upgrading to Boot 3, the response body changed to:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"instance": "/foo/99"
}
I couldn't find any related information in the Spring Boot 3.0 Migration Guide nor in the Upgrading to Spring Framework 6.x one.

Spring Web 6 introduced support for the "Problem Details for HTTP APIs" specification, RFC 7807.
With this, the ResponseStatusException now implements the ErrorResponse interface and extends the ErrorResponseException class.
Having a quick look at the javadocs, we can see that all these are backed by the RFC 7807 formatted ProblemDetail body, which, as you can imagine, has the fields of the new response you're getting, and also uses the application/problem+json media type in the response.
Here is a reference to how Spring now treats Error Responses, which naturally goes in the direction of using Problem Details spec across the board.
Now, normally, if you were simply relying on Boot's Error Handling mechanism without any further change, you would still see the same response as before. My guess is that you are using a #ControllerAdvice extending ResponseEntityExceptionHandler. With that, you are enabling RFC 7807 (as per the Spring docs here)
So, that is why your ResponseStatusException has changed its body content.
Configuring the Problem Detail response body to include previous fields
If you need to stick to the pre-existing fields (at least until you fully migrate to the Problem Detail based approach) or if you simply want to add custom fields to the error response, you can override the createResponseEntity method in the #ControlAdvice class extending ResponseEntityExceptionHandler as follows:
#ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> createResponseEntity(#Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
if (body instanceof ProblemDetail) {
ProblemDetail problemDetail = ((ProblemDetail) body);
problemDetail.setProperty("error", problemDetail.getTitle());
problemDetail.setProperty("timestamp", new Date());
if (request instanceof ServletWebRequest) {
problemDetail.setProperty("path", ((ServletWebRequest) request).getRequest()
.getRequestURI());
}
}
return new ResponseEntity<>(body, headers, statusCode);
}
}
Note: I'm using new Date() instead of Java Time simply because that's what Boot's DefaultErrorAttributes class uses. Also, I'm not using Boot's ErrorAttributes for simplicity.
Note that defining the path field is a little bit tricky because problemDetail.getInstance() returns null at this stage; the framework sets it up later in the HttpEntityMethodProcessor.
Of course, this solution is suitable for a servlet stack, but it should help figure out how to proceed in a reactive stack as well.
With this, the response will look as follows:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"instance": "/foo/99",
"error": "Not Found",
"path": "/foo/99",
"timestamp": "2023-01-06T10:00:20.509+00:00"
}
Of course, it has duplicated fields. You can completely replace the response body in the method if you prefer.
Configuring Boot to also use the Problem Detail spec
Now, to be consistent across the board in your application, note that Boot now provides the spring.mvc.problemdetails.enabled property to use the Problem Details in its Error Handling mechanism (which is disabled by default to avoid breaking changes, as its associated issue):
spring.mvc.problemdetails.enabled=true

Related

How to display validation error messages and codes in a Spring WebFlux Rest API

I'm using Spring Web MVC in a Reactive SpringBoot Application, and wrote a custom validator. In case of a validation error, a 400 response code is used, which is fine, but then AbstractErrorWebExceptionHandler catches this and spits out
{
"timestamp": 1651678946524,
"path": "/signup",
"status": 400,
"error": "Bad Request",
"requestId": "0f61cb96-1"
}
which is not very useful to the client. How can I display the error messages and codes? I know they are available, because they are logged:
DEBUG 32475 --- [ parallel-1] a.w.r.e.AbstractErrorWebExceptionHandler : [0f61cb96-1] Resolved [WebExchangeBindException: Validation failed for argument at index 0 in method: public reactor.core.publisher.Mono<...with 1 error(s): [Field error in object 'userSignup' on field 'username': rejected value [matilda0]; codes [username.exists.userSignup.username,username.exists.username,username.exists.java.lang.String,username.exists]; arguments []; default message [null]] ] for HTTP POST /signup
Any help would be appreciated, thank you.
This is what I did:
private void validate(SomeEntity someEntity) {
Errors errors = new BeanPropertyBindingResult(someEntity, "SomeEntity");
validator.validate(someEntity, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString());
}
}
Validator is injected:
private final Validator validator;
private final SomeEntityDao dao;
public SomeEntityHandler(
Validator validator,
SomeEntityDao dao
) {
this.validator = validator;
this.dao = dao;
}
Project: WebFluxR2dbc
Adding
server.error.include-binding-errors=ALWAYS
in application.properties seems to fix it, although it still doesn't look up the message code properly. To actually get the error message itself to appear in the response, I had to wire in a MessageSourceAccessor in my Validator and use that as the default message!
errors.rejectValue("username","username.exists",msgs.getMessage("username.exists"))
So I must still be missing something, but this will work for now.

How to handle Bad Request to return custom Response object for PostRequest in Spring Boot Rest Api

I am Trying to create Api which can accept POST Request.
But I want to handle Bad Request Also.
My Json object will be like
JSON Object
{
"name": "tom"
"description" : "he is scholar"
}
JSON object with incorrect parameter
{
"name": "tom"
"descr" : "he is scholar"
}
#PostMapping("/questions")
public question addQuestion(#RequestBody question theQuestion) {
theQuestion.setId(0);
try {
thequestionService.save(theQuestion);
}catch(Exception ex) {
throw new badRequestException("bad request");
}
return theQuestion;
}
For Bad Request it is throwing spring Boot internel error.
But I want to send custom JSON object to back to client.
Response in case of failure 400 Bad Request.
{
"status":"failed to query"
"description" : " can be any thing"
}
Help me out for this issue.
You can use #JsonAlias annotation.
public class Question {
#JsonAlias(value={"description", "descr"})
private String description;
}
This annotation was added in Jackson 2.9. You might have to upgrade/override Jackson version.
For Bad Request it is throwing spring Boot internel error.
You can also add #JsonIgnoreProperties(ignoreUnknown = true) annotation to Question class. Doing this will not throw error for descr key in JSON, but will initialize description field in Question object as null.

Change body of ServerWebExchange response in WebFilter

I am trying to make an application in spring webflux. But there is a lot of stuff that I can't find in the documentation.
I have a webfilter where I want to add a wrapper around the response. Example:
Reponse before:
{
"id": 1,
"name": "asdf"
}
Response after:
{
"status": "success",
"data": {
"id": 1,
"name": "asdf"
}
}
This is the WebFilter at the moment:
#Component
public class ResponseWrapperFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
// Add wrapper to the response content...
return webFilterChain.filter(serverWebExchange);
}
}
But I can't find a way to get or edit the body of the response.
How can I change the response output in a WebFilter?
As stated in the javadoc, WebFilter is made for application-agnostic, cross-cutting concerns. WebFilters usually mutate the request headers, add attributes to the exchange or handle the response altogether without delegating to the rest of the chain.
I don't think that what you're trying to achieve is easily doable or even something that should be achieved this way. At the raw exchange level, you're dealing with a Flux<DataBuffer>, which is the response body split randomly in groups of bytes.
You could somehow wrap the response in a filter and use Flux.concat to prepend and append data to the actual response written by the handler. But there are a lot of questions regarding encoding, infinite streams, etc.
Maybe this is a concern that should be achieved at the Encoder level, since there you could restrict that behavior to a particular media type.

Return all available mappings on RequestMethod GET

I'm learning about spring boot and having a REST Server with JPA.
For my RestControllers, I wanted to have the behavior on the base page that when someone goes to the base page they would be able to see all the available RequestMappings under the base page.
#RestController
#RequestMapping("/food")
public class FoodRestController {
#RequestMapping(value = "/all", method = RequestMethod.GET)
#ResponseBody
public Iterable<Food> printAllFoods() {
return foodRepository.findAll();
}
#RequestMapping(value = "/add", method = RequestMethod.POST)
public ResponseEntity<?> addFood(#RequestBody Food f) {
foodRepository.save(f);
HttpHeaders httpHeaders = new HttpHeaders();
return new ResponseEntity<Food>(f, httpHeaders, HttpStatus.CREATED);
}
So for the above going to "localhost:8080/food" would give a page showing something like not a valid endpoint, possible endpoints are localhost:8080/food/all or localhost:8080/food/add.
I could just have a RequestMapping with GET and return it as a body but it would be a manually typed response. Wanted to see if Spring offers anything like this
You can also use Swagger . It is actually a documentation framework. It also builds around a beautiful UI to try out the APIs that are available along with the documentation.
The SpringBoot Actuator already has a functionality that does something like this. Add a dependency on the SpringBoot Actuator, start up your application and point your browser to:
http://[yourHostAndPort]/mappings
This will produce something like the following (assuming JSON), which contains ALL mappings that are a part of your project (Spring endpoints too!).
{
...
"{[/refresh],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}": {
"bean": "endpointHandlerMapping",
"method": "public java.lang.Object org.springframework.cloud.endpoint.GenericPostableMvcEndpoint.invoke()"
},
"{[/restart],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}": {
"bean": "endpointHandlerMapping",
"method": "public java.lang.Object org.springframework.cloud.context.restart.RestartMvcEndpoint.invoke()"
},
"{[/configprops],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}": {
"bean": "endpointHandlerMapping",
"method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
},
"{[/env],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}": {
"bean": "endpointHandlerMapping",
"method": "public java.lang.Object org.springframework.cloud.context.environment.EnvironmentManagerMvcEndpoint.value(java.util.Map<java.lang.String, java.lang.String>)"
},
...
}
This snippet shows a small handful of the mappings that are available from some other Actuator endpoints.
I understand that your requirements are a bit different, so if this setup isn't exactly what you need, you should be able to create your own endpoint(s) that do something similar, just by browsing the SpringBoot Actuator source code. The specific file that does the majority of the work for the mappings endpoint is org.springframework.boot.actuate.endpoint.RequestMappingEndpoint.java.

How to make a #RestController POST method ignore Content-Type header and only use request body?

I'm using latest Spring Boot (1.2.1) and whatever Spring MVC version comes with it.
I have a controller method with implicit JSON conversions for both incoming and outgoing data:
#RestController
public class LoginController {
#RequestMapping(value = "/login", method = POST, produces = "application/json")
ResponseEntity<LoginResponse> login(#RequestBody LoginRequest loginRequest) {
// ...
}
}
This works fine, but only if request Content-Type is set to application/json. In all other cases, it responds with 415, regardless of the request body:
{
"timestamp": 1423844498998,
"status": 415,
"error": "Unsupported Media Type",
"exception": "org.springframework.web.HttpMediaTypeNotSupportedException",
"message": "Content type 'text/plain;charset=UTF-8' not supported",
"path": "/login/"
}
Thing is, I'd like to make my API more lenient; I want Spring to only use the POST request body and completely ignore Content-Type header. (If request body is not valid JSON or cannot be parsed into LoginRequest instance, Spring already responds with 400 Bad Request which is fine.) Is this possible while continuing to use the implicit JSON conversions (via Jackson)?
I've tried consumes="*", and other variants like consumes = {"text/*", "application/*"} but it has no effect: the API keeps giving 415 if Content-Type is not JSON.
Edit
It looks like this behaviour is caused by MappingJackson2HttpMessageConverter whose documentation says:
By default, this converter supports application/json and
application/*+json. This can be overridden by setting the supportedMediaTypes property.
I'm still missing how exactly do I customise that, for example in a
custom Jackson2ObjectMapperBuilder...
I assume that you are using default MappingJackson2HttpMessageConverter provided by Spring.
If you would like to have the same behavior in all requests, one solution would be to write custom converter which will not look for Content-Type, in a header (instead will parse to JSON alwayse) and then configure Spring to use your custom one. Again this will affect all requests, so might not fit all needs.
public class CustomerJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
private ObjectMapper mapper = new ObjectMapper();
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
public CustomerJsonHttpMessageConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET));
}
#Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return mapper.readValue(inputMessage.getBody(), clazz);
}
#Override
protected boolean supports(Class<?> clazz) {
return true;
}
#Override
protected void writeInternal(Object value, HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
String json = mapper.writeValueAsString(value);
outputMessage.getBody().write(json.getBytes());
}
}
To have custom media type,
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(
Arrays.asList(
new MediaType("text", "plain"),
new MediaType("text", "html")
));
For anyone else who is curious about this;
It is possible to customize the used MappingJackson2HttpMessageConverter by overridding WebMvcConfigurerAdapter.extendMessageConverters to allow for multiple mime types.
However, it does not work as expected because application/x-www-form-urlencoded is hardcoded in ServletServerHttpRequest.getBody to modify the body to be url encoded (even if the post data is JSON) before passing it to MappingJackson2HttpMessageConverter.
If you really needed this to work then I think the only way is to put a Filter that modifies the request content-type header before handling (not to imply this is a good idea, just if the situation arises where this is necessary).
Update: watch out if you use this
(This was probably a stupid idea anyway.)
This has the side effect that server sets response Content-Type to whatever the first value in the request's Accept header is! (E.g. text/plain instead of the correct application/json.)
After noticing that, I got rid of this customisation and settled went with Spring's default behaviour (respond with 415 error if request does not have correct Content-Type).
Original answer:
MappingJackson2HttpMessageConverter javadocs state that:
By default, this converter supports application/json and application/*+json. This can be overridden by setting the supportedMediaTypes property.
...which pointed me towards a pretty simple solution that seems to work. In main Application class:
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter =
new MappingJackson2HttpMessageConverter(new CustomObjectMapper());
converter.setSupportedMediaTypes(Arrays.asList(MediaType.ALL));
return converter;
}
(CustomObjectMapper is related to other Jackson customisations I have; that contructor parameter is optional.)
This does affect all requests, but so far I don't see a problem with that in my app. If this became a problem, I'd probably just switch the #RequestBody parameter into String, and deserialise it manually.

Resources