Change default "type" for problem detail API in Spring Boot? - spring-boot

I'm using Spring Boot 3.x and I have a controller defined like this:
#RestController
#RequestMapping(path = ["/my-controller"])
#Validated
class MyController {
private val log = loggerFor<MyController>()
#PutMapping("/{something}", consumes = [APPLICATION_JSON_VALUE])
#ResponseStatus(code = HttpStatus.NO_CONTENT)
fun test(
#PathVariable("something") something: String,
#Valid #RequestBody someDto: SomeDTO
) {
log.info("Received $someDto")
}
}
data class SomeDTO(val myBoolean: Boolean)
I've also enabled problem details (RFC 7807) in my application.yaml file:
spring:
mvc:
problemdetails:
enabled: true
When I make a request (in this example I'm using rest assured) to /my-controller/hello with a json body that (intentionally) doesn't match the expected data (myBoolean is not a valid boolean):
Given {
port(<port>)
contentType(JSON)
body("""{ "myBoolean" : "not a boolean"}""")
log().all()
} When {
put("/my-controller/hello")
} Then {
log().all().
statusCode(400)
}
Then the response body looks like this:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Failed to read request",
"instance": "/my-controller/hello"
}
My question is, how can I change the default type from about:blank to something else?

You need a #ControllerAdvice defined as follows:
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(Exception.class)
public ResponseEntity<Problem> handleAllExceptions(Exception ex, WebRequest request) {
return ResponseEntity.of(
Optional.of(
Problem.builder()
.withType(URI.create("https://foobar.com/problem-definitions/blah"))
.withTitle("Bad Request")
.withDetail(ex.getMessage())
.withStatus(Status.BAD_REQUEST)
.build()
));
}
}
It returns this for your example:
{
"type": "https://foobar.com/problem-definitions/blah",
"title": "Bad Request",
"status": 400,
"detail": "Type definition error: [simple type, class com.example.demo.web.LanguageController$SomeDTO]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.demo.web.LanguageController$SomeDTO`: non-static inner classes like this can only by instantiated using default, no-argument constructor\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 3]"
}
I used this dependency:
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web-starter</artifactId>
<version>0.28.0-RC.0</version>
</dependency>
Note that ProblemDetail is Spring Framework 6. Implementation in Spring 6 looks like this:
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(Exception.class)
public ResponseEntity<ProblemDetail> handleAllExceptions(Exception ex, WebRequest request) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
problemDetail.setType(URI.create("https://foobar.com/problem-definitions/blah"));
problemDetail.setInstance(URI.create("https://instance"));
return ResponseEntity.of(Optional.of(problemDetail));
}

Related

Why do I get random Http 404 from server between same requests with only one change in any field?

I haven an endpoint POST /api/marketplace/add that accepts a DTO object as request body. When I send the body below with platformName field set , server accepts request and processes it with no problem. But when I only try to change field platformName to null I get Http 404 error from server. I debugged the request and found out that it even can not reach controller method. I also got no trace from that error. What might be the cause that makes API respond differently to same request?
below
{
"platformName": "Trendyol",
"commissionAmounts": [
{
"amount": 23.45,
"categoryInfos": [
{
"categoryName": "Game"
}
],
"isCategoryBasedPricing": true
}
],
"shipmentAmounts": [
{
"amount": 23.45,
"scaleInfo": {
"order": 0,
"lowerBound": 0,
"upperBound": 0
},
"volumeInfo": {
"order": 0,
"lowerBound": 0,
"upperBound": 0
},
"isVolumeBasedPricing": true
}]
}
EDIT: dto model is
#Generated
public class MarketPlaceDTO {
#JsonProperty("platformName")
private String platformName;
#JsonProperty("commissionAmounts")
#Valid
private List<CommissionInfoDTO> commissionAmounts = new ArrayList<>();
#JsonProperty("shipmentAmounts")
#Valid
private List<ShipmentInfoDTO> shipmentAmounts = new ArrayList<>();
Controller is implementing swagger generated api interface. with postmapping and requestbody annotations.
#RequiredArgsConstructor
#RestController
public class MarketPlaceApiController implements MarketplaceApi {
private final MarketPlaceDAOService marketPlaceDAOService;
#Override
public ResponseEntity<BaseResponseDTO> addMarketPlace(MarketPlaceDTO
marketPlaceDTO) {
BaseResponseDTO dto =
marketPlaceDAOService.addMarketPlace(marketPlaceDTO);
return ResponseEntity.ok(dto);
}
}
Swagger generated api interface
#RequestMapping(
method = RequestMethod.POST,
value = "/marketplace/add",
produces = { "application/json", "application/xml" },
consumes = { "application/json" })
default ResponseEntity<BaseResponseDTO> _addMarketPlace(
#Parameter(name = "MarketPlaceDTO", description = "Add new
marketplace with given request body", required = true) #Valid
#RequestBody MarketPlaceDTO marketPlaceDTO) {
return addMarketPlace(marketPlaceDTO);
}
Response is
{
"timestamp": 1666866382906,
"status": 404,
"error": "Not Found",
"path": "/marketplace/add"
}
Obviously, that you use an endpoint with #RequestBody where body is a DTO.
And on trying to call this endpoint Spring Web first should match that a model in your request payload matches a require object in #RequestBody argument.
Ideally, using DTO as a request model is not a good idea. But I don't see your structure and cannot say if it's a problem or not.
The simple solution in your case is preparation (annotating) your DTO with specific JSON annotations:
#JsonInclude
#JsonIgnoreProperties(ignoreUnknown = true)
public class YourDTO {
private String platformName;
}
and for Controller add class annotation #Validated; for #RequestBody add #Valid annotation.
Recommendation: use request models for incoming objects, and later converters to DTO/entities with ability to response them with filtering (or in complex cases add also response model - usually it's overhead).
My problem was global exception handler component annotated with #ControllerAdvice. I tried to handle validation exceptions and forgot to add #ResponseBody to my handler methods which is in my case probabaly required. That somehow caused server to send http 404 message when any input validation exception was thrown. After I made changes , Exceptions was handled correctly by handler component.
#ControllerAdvice
#ResponseBody // this resolved my issue.
public class MVCExceptionHandler {
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseErrorResponse
methodArgumentExceptions(MethodArgumentNotValidException e){
return BaseErrorResponse.builder()
.errorMessage(AppError.INVALID_OR_MISSING_USER_INPUT.getErrorMessage())
.errorCode(AppError.INVALID_OR_MISSING_USER_INPUT.getErrorCode())
.errorTime(Date.from(Instant.now())).build();
}

What is the proper way to control Required request body is missing Exception throwing?

I'm developing an API Service using Spring Boot with Maven. The problem is I want to control the Required request body is missing exception that is thrown to the client.
For example, I provide a API with POST method to the client. When the client call the API without Body. The Spring Boot will throw error in the body response like this,
{
"timestamp": "2021-09-14T18:05:47.992+00:00",
"status": 400,
"error": "Bad Request",
"trace": "org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public org.springframework.http.ResponseEntity<java.lang.Object>....
...
...
}
This will display the Controller name and line of code to the client. How can I just return some object to the client and like this,
{
"message": "Required request body is missing"
}
Thank you for every helps.
What you are looking for is a custom exception handler implementation. You need to override the following method in your custom exception handler.
The code would look somewhat like this:
#ControllerAdvice
#RestController
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
final MyMessageDto myExMsgDTO = new MyMessageDto("Required request body is missing");
return new ResponseEntity(myExMsgDTO, headers, status);
}
}
Here, your MyMessageDto class can be a simple POJO like this:
public class MyMessageDto {
private String message;
public MyMessageDto(String message) {
super();
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
You can add more variables in the DTO class mentioned above to give more details in case of exception.

Webclient ExchangeFilter not return a defined custom exception class

I have a problem when i do a webclient request (to a external api) and the response is 4xx ou 5xx code. The propose is handling that response and retrieve a response with a custom class
The webclient configuration is
return WebClient.builder()
.baseUrl(baseUrl)
.defaultHeaders(httpHeaders -> {
httpHeaders.setBearerAuth("token");
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
})
.filter(handlingFilter())
.build();
}
handlingFilter.class
private static ExchangeFilterFunction handlingFilter() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if(clientResponse.statusCode()!=null && (clientResponse.statusCode().is5xxServerError() || clientResponse.statusCode().is4xxClientError()) ) {
return Mono.error(new MyException(clientResponse.statusCode().value(), clientResponse.statusCode().getReasonPhrase()));
}else {
return Mono.just(clientResponse);
}
});
}
MyExpcetion.class
public class MyException extends Exception{
private int code;
private String message;
public MyException(String message) {
super(message);
}
}
But my client responses always give me a default format
{
"timestamp": "x",
"path": "x",
"status": "x",
"error": "x",
"message": "x",
"requestId": "x",
}
instead of
{
"code": "x",
"message": "x"
}
what's wrong ?
thanks
To change your client's response (the response of your endpoint), you have to handle the exception properly. Take a look on Spring Documentation about Managing Exceptions
Resuming: if you are using annotation endpoints, you have to create a #ExceptionHandler(MyException.class) on your Controller class or in a #RestControllerAdvice.
If you are using Functional Endpoints, then configure WebExceptionHandler

Exception message not included in response when throwing ResponseStatusException in Spring Boot

My Spring Boot application provides the following REST controller:
#RestController
#RequestMapping("/api/verify")
public class VerificationController {
final VerificationService verificationService;
Logger logger = LoggerFactory.getLogger(VerificationController.class);
public VerificationController(VerificationService verificationService) {
this.verificationService = verificationService;
}
#GetMapping
public void verify(
#RequestParam(value = "s1") String s1,
#RequestParam(value = "s2") String s2) {
try {
verificationService.validateFormat(s1, s2);
} catch (InvalidFormatException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
}
}
}
In case validateFormat() throws the InvalidFormatException the client gets a HTTP 400 which is correct. The default JSON response body however looks like this:
{
"timestamp": "2020-06-18T21:31:34.911+00:00",
"status": 400,
"error": "Bad Request",
"message": "",
"path": "/api/verify"
}
The message value is always empty even if I hard-code it like this:
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "some string");
This is the exception class:
public class InvalidFormatException extends RuntimeException {
public InvalidFormatException(String s1, String s2) {
super(String.format("Invalid format: [s1: %s, s2: %s]", s1, s2));
}
}
This behavior has changed with Spring Boot 2.3 and is intentional. See release notes for details.
Setting server.error.include-message=always in the application.properties resolves this issue.
Setting server.error.include-message=always disclosures messages of internal exceptions and this might be a problem in production environment.
An alternative approach is to use ExceptionHandler. Here you can control what is transferred to client:
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<String> handleBadRequestException(ResponseStatusException ex) {
// if you want you can do some extra processing with message and status of an exception
// or you can return it without any processing like this:
return new ResponseEntity<>(ex.getMessage(), ex.getStatus());
}
}

Spring Boot catch multiple exceptions and send as error response

I am validating an incoming POST request which will create a database entity after validating the request data. I am trying to gather multiple errors in a single request and respond as error response following JSON API spec:
https://jsonapi.org/examples/#error-objects-multiple-errors
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
"errors": [
{
"status": "403",
"source": { "pointer": "/data/attributes/secretPowers" },
"detail": "Editing secret powers is not authorized on Sundays."
},
{
"status": "422",
"source": { "pointer": "/data/attributes/volume" },
"detail": "Volume does not, in fact, go to 11."
},
{
"status": "500",
"source": { "pointer": "/data/attributes/reputation" },
"title": "The backend responded with an error",
"detail": "Reputation service not responding after three requests."
}
]
}
Is it possible to do this by #ControllerAdvice. When Global exception handling is enabled by #ControllerAdvice and throws an exception, the next exception won't be caught.
Not directly, no. Not sure what is your business case/logic, therefore I don't know how you handling these exceptions in service layer, but in general, if you want to pass multiple errors in your #ExceptionHanlder - you could create a custom POJO:
public class MyError {
private String status;
private String source;
private String title;
private String detail;
getters/setters...
}
and then create a custom RuntimeException which would accept list of these POJOs:
public class MyRuntimeException extends RuntimeException {
private final List<MyError> errors;
public MyRuntimeException(List<MyError> errors) {
super();
this.errors = errors;
}
public List<MyError> getErrors() {
return errors;
}
}
And in your service layer you could create list of these POJOs, wrap then in your exception and throw it. Then in #ControllerAdvice you simply catch your exception and call accessor method to iterate against your list of POJOs to construct a payload you want.
Something like:
#ExceptionHandler (MyRuntimeException.class)
#ResponseStatus (BAD_REQUEST)
#ResponseBody
public Map<String, Object> handleMyRuntimeException(MyRuntimeException e) {
return singletonMap("errors", e.getErrors());
}

Resources