I have Spring Boot MVC application where exceptions are handled within a general #ControllerAdvice. Some of them do not include a response body, like for instance:
#ExceptionHandler(EntityNotFoundException.class)
#ResponseStatus(NOT_FOUND)
void handleEntityNotFound() {
}
Everything works fine, but I run into issues if I want to expose my endpoints with SpringDoc. The exception handler is picked up correctly, however, the Swagger-UI displays a random response for 404s:
Note, that this even isn't the response of the GET endpoint in question here, but a response from a method from a different RestController.
What do I have to do to hide wrong response? I already tried
#ApiResponse(responseCode = "404", description = "Not found", content = #Content)
#ExceptionHandler(EntityNotFoundException.class)
#ResponseStatus(NOT_FOUND)
void handleEntityNotFound() {
}
as suggested in the docs, but that does not work.
Your issue is resolved in v1.4.1:
https://github.com/springdoc/springdoc-openapi/issues/711
If you don't want to hide it and want to show the appropriate response, one way to do that is you can define a string as an example
public static final String exampleInternalError = "{\r\n"
+ " \"code\": 404,\r\n"
+ " \"message\": \"Resource not found\"\r\n" + "}";
same is used to show the example as
#ApiResponse(responseCode = "404",
description = "Resource not found",
//And different example for this error
content = #Content(schema = #Schema(implementation = ErrorResponse.class),
examples = #ExampleObject(description = "Internal Error", value = exampleInternalError)))
Related
Assume following code written with Quarkus. But can as well be with micronaut.
#POST
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
#APIResponses(
value = {
#APIResponse(
responseCode = "201",
description = "Customer Created"),
#APIResponse(
responseCode = "400",
description = "Customer already exists for customerId")
}
)
public Response post(#Valid Customer customer) {
final Customer saved = customerService.save(customer);
return Response.status(Response.Status.CREATED).entity(saved).build();
}
The Customer definition includes a field pictureUrl. CustomerService is responsible to validate the the URL is a valid URL and that the image really exists.
This means that following exception will be processed by the service: MalformedURLException and IOException. The CustomerService catches these errors and throws an application specific exception to report that the image does not exist or the path is not correct: ApplicationException.
How do you document this error case with microprofile?
My research suggests that I have to implement an exception mapper of the form:
public class ApplicationExceptionMapper implements ExceptionMapper<NotFoundException> {
#Override
#APIResponse(responseCode = "404", description = "Image not Found",
content = #Content(
schema = #Schema(implementation = Customer.class)
)
)
public Response toResponse(NotFoundException t) {
return Response.status(404, t.getMessage()).build();
}
}
And once I have a such mapper, the framework would know how to convert my exception into Response. Is my analysis correct? What is the best practice?
You are more or less pointed in the right direction, your question can be divided in two, let me answer separately both:
How to document an error using microprofile openapi: Using the api responses and the operation description is the correct way as you are doing, you can include a extended description of your errors and the specific Http error code associated with each if you want. This annotations should be present in the Rest Resource not in the ExceptionMapper.
Handling custom errors with micro profile or just the Jax-RS way of dealing with exceptions: A endpoint implemented with Jax-RS Apis knows how to hande WebApplicationExceptions automatically, By launching a custom exception which has this one us parent, the Jax-RS implementation automatically will know how to map your Exception to a Response.
Dealing with Unexpected exceptions or customizing responses for certain exceptions: By implementing the interface ExceptionMapper, you can further customize the response generation for specific exceptions if you want but this is not generally required.
A common good strategy for dealing with the scenario you have explained is to have a family of custom exceptions that extend the WebApplicationException where you can specify the response code and message, Or just using the ones provided by jax-rs. And if you need further customization with i18n support or its important to provide a response entity with details associated to the error, then implement the ExceptionMapper.
For the actual error handling and conversion of exceptions to corresponding HTTP responses you would use the JaxRS exception mapper as you already started. But the exception mapper itself, is not considered at all during the creation of the OpenAPI schema, as the OpenAPI extension has no way of obtaining the information about the actual error responses produced by your exception mapper. So you need to declare the possible error cases on the actual endpoint methods -which can be a bit tedious, especially if your endpoint methods can result in multiple error responses (400, 500, 409..). But you can reduce the amount of duplicated code by creating a shared error response definitions.
A common pattern for error handling on API layer is to explicitly define the models for your error responses e.g. I want all my endpoints to return error responses in a form of json document that looks sth like this:
{
"errorCode" : "MY_API_123",
"errorDescription" : "This operation is not allowed"
//anything else, details, links etc
}
So I create a POJO model for the response:
#Data
#AllArgsConstructor
public class ErrorResponse {
private String errorCode;
private String errorDescription;
}
And in my exception mapper I can convert my exception to the error response above:
public Response toResponse(Exception e) {
// you can define a mapper for a more general type of exception and then create specific error responses based on actual type, or you could define your own application level exception with custom error info, keys, maybe even i18n, that you catch here, you get the idea
if (e instanceOf NotFoundException) {
return Response.status(404, new ErrorResponse("MY_API_400", "Object not found")).build();
}
if (e instanceOf IllegalArgumentException) {
return Response.status(400, new ErrorResponse("MY_API_400", "Invalid request")).build();
}
// fallback if we dont have any better error info
return Response.status(500, new ErrorResponse("MY_API_100", "Internal server error")).build();
}
You can then document the possible error responses using the OpenAPi annotations:
#APIResponses({
#APIResponse(responseCode = "201", description = "Customer Created"),
#APIResponse(responseCode = "400", description = "Other object exists for customerId", content = #Content(schema = #Schema(implementation = ErrorResponse.class))),
#APIResponse(responseCode = "500", description = "Internal error", content = #Content(schema = #Schema(implementation = ErrorResponse.class)))
})
public Response yourMethod()
Or if you have some responses that are repeated often (such as generic internal server error, unathorized/unathenticated) you can document them on your class extending JaxRS Application and then reference them in your endpoints like this:
#Authenticated
#APIResponses({
#APIResponse(responseCode = "200", description = "Ok"),
#APIResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"),
#APIResponse(responseCode = "500", ref = "#/components/responses/ServerError")
})
public Response someAPIMethod() {
Example JaxRS application class(useful for other common top level openapi schema attributes)
#OpenAPIDefinition(
info = #Info(title = "My cool API", version = "1.0.0"),
components = #Components(
schemas = {#Schema(name = "ErrorResponse", implementation = ErrorResponse.class)},
responses = {
#APIResponse(name = "ServerError", description = "Server side error", content = #Content(mediaType = MediaType.APPLICATION_JSON, schema = #Schema(ref = "#/components/schemas/ErrorResponse")), responseCode = "500"),
#APIResponse(name = "NotFound", description = "Requested object not found", content = #Content(schema = #Schema(ref = "#/components/schemas/ErrorResponse")), responseCode = "404"),
#APIResponse(name = "Forbidden", description = "Authorization error", responseCode = "403"),
#APIResponse(name = "BadRequest", description = "Bad Request", responseCode = "400"),
#APIResponse(name = "Unauthorized", description = "Authorization error", responseCode = "401")})
)
public class RESTApplication extends Application {
}
I have the following controller method:
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, path = "/upload")
public Mono<SomeResponse> saveEnhanced(#RequestPart("file") Mono<FilePart> file) {
return documentService.save(file);
}
which calls a service method where I try to use a WebClient to put some data in another application:
public Mono<SomeResponse> save(Mono<FilePart> file) {
MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder();
bodyBuilder.asyncPart("file", file, FilePart.class);
bodyBuilder.part("identifiers", "some static content");
return WebClient.create("some-url").put()
.uri("/remote-path")
.syncBody(bodyBuilder.build())
.retrieve()
.bodyToMono(SomeResponse.class);
}
but I get the error:
org.springframework.core.codec.CodecException: No suitable writer found for part: file
I tried all variants of the MultipartBodyBuilder (part, asyncpart, with or without headers) and I cannot get it to work.
Am I using it wrong, what am I missing?
Regards,
Alex
I found the solution after getting a reply from one of the contributes on the Spring Framework Github issues section.
For this to work:
The asyncPart method is expecting actual content, i.e. file.content(). I'll update it to unwrap the part content automatically.
bodyBuilder.asyncPart("file", file.content(), DataBuffer.class)
.headers(h -> {
h.setContentDispositionFormData("file", file.name());
h.setContentType(file.headers().getContentType());
});
If both headers are not set then the request will fail on the remote side, saying it cannot find the form part.
Good luck to anyone needing this!
I am currently testing one of my services with Spring boot test.The service exports all user data and produces a CSV or PDF after successful completion. A file is downloade in browser.
Below is the code i have wrote in my test class
MvcResult result = MockMvc.perform(post("/api/user-accounts/export").param("query","id=='123'")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_PDF_VALUE)
.content(TestUtil.convertObjectToJsonBytes(userObjectDTO)))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_PDF_VALUE))
.andReturn();
String content = result.getResponse().getContentAsString(); // verify the response string.
Below is my resource class code (call comes to this place)-
#PostMapping("/user-accounts/export")
#Timed
public ResponseEntity<byte[]> exportAllUsers(#RequestParam Optional<String> query, #ApiParam Pageable pageable,
#RequestBody UserObjectDTO userObjectDTO) {
HttpHeaders headers = new HttpHeaders();
.
.
.
return new ResponseEntity<>(outputContents, headers, HttpStatus.OK);
}
While I debug my service, and place debug just before the exit, I get content Type as 'application/pdf' and status as 200.I have tried to replicate the same content type in my test case. Somehow it always throws below error during execution -
java.lang.AssertionError: Status
Expected :200
Actual :406
I would like to know, how should i inspect my response (ResponseEntity). Also what should be the desired content-type for response.
You have problem some where else. It appears that an exception/error occurred as noted by application/problem+json content type. This is probably set in the exception handler. As your client is only expecting application/pdf 406 is returned.
You can add a test case to read the error details to know what exactly the error is.
Something like
MvcResult result = MockMvc.perform(post("/api/user-accounts/export").param("query","id=='123'")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_PROBLEM_JSON_VALUE)
.content(TestUtil.convertObjectToJsonBytes(userObjectDTO)))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE))
.andReturn();
String content = result.getResponse().getContentAsString(); // This should show you what the error is and you can adjust your code accordingly.
Going forward if you are expecting the error you can change the accept type to include both pdf and problem json type.
Note - This behaviors is dependent on the spring web mvc version you have.
The latest spring mvc version takes into account the content type header set in the response entity and ignores what is provided in the accept header and parses the response to format possible. So the same test you have will not return 406 code instead would return the content with application json problem content type.
I found the answer with help of #veeram and came to understand that my configuration for MappingJackson2HttpMessageConverter were lacking as per my requirement. I override its default supported Mediatype and it resolved the issue.
Default Supported -
implication/json
application*/json
Code change done to fix this case -
#Autowired
private MappingJackson2HttpMessageConverter jacksonMessageConverter;
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.ALL);
jacksonMessageConverter.setSupportedMediaTypes(mediaTypes);
406 means your client is asking for a contentType (probably pdf) that the server doesn't think it can provide.
I'm guessing the reason your code is working when you debug is that your rest client is not adding the ACCEPT header that asks for a pdf like the test code is.
To fix the issue, add to your #PostMapping annotation produces = MediaType.APPLICATION_PDF_VALUE see https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/PostMapping.html#produces--
Hi It might look like duplicate but its not.
I am building a rest api using spring boot and need to fetch form-data sent by client app in POST request.
for testing purpose I am using postman. So far i have tried below
#PostMapping("/feed/comment/add/{feedId}")
public ResponseEntity<BaseResponse> addComment(#RequestHeader(name = Constants.USER_ID_HEADER) int userId,
#PathVariable("feedId") int feedId,
#RequestParam("comment") String comment
) {
LOGGER.info("Received add comment request with comment:"+comment);
return new ResponseEntity<BaseResponse>(new BaseResponse("You are not feed owner", RESPONSETYPE.ERROR), HttpStatus.UNAUTHORIZED);
}
this gives error "Required String parameter 'comment' is not present"
Second way tried:
#PostMapping("/feed/comment/add/{feedId}")
public ResponseEntity<BaseResponse> addComment(#RequestHeader(name = Constants.USER_ID_HEADER) int userId,
#PathVariable("feedId") int feedId,
#RequestParam Map<String, String> values
) {
for(String key: values.keySet()) {
System.out.println(key+":"+values.get(key));
}
return new ResponseEntity<BaseResponse>(new BaseResponse("You are not feed owner", RESPONSETYPE.ERROR), HttpStatus.UNAUTHORIZED);
}
this gives wired output:
------WebKitFormBoundarymk97RU1BbJyR0m3F
Content-Disposition: form-data; name:"comment"
test comment
------WebKitFormBoundarymk97RU1BbJyR0m3F--
I'm pretty sure that with plane servlet i can access this using request.getParameter("comment")
not sure how i can fetch it in case of spring rest controller.
"Required String parameter 'comment' is not present" this error happens when this paremeter is required but you didn't send it.
#RequestParam(value="comment", required=false)
this will make the comment parameter optional. So if you missed sending the comment parameter its ok.
I have the following code in my web application:
#ExceptionHandler(InstanceNotFoundException.class)
#ResponseStatus(HttpStatus.NO_CONTENT)
public ModelAndView instanceNotFoundException(InstanceNotFoundException e) {
return returnErrorPage(message, e);
}
Is it possible to also append a status message to the response? I need to add some additional semantics for my errors, like in the case of the snippet I posted I would like to append which class was the element of which the instance was not found.
Is this even possible?
EDIT: I tried this:
#ResponseStatus(value=HttpStatus.NO_CONTENT, reason="My message")
But then when I try to get this message in the client, it's not set.
URL u = new URL ( url);
HttpURLConnection huc = (HttpURLConnection) u.openConnection();
huc.setRequestMethod("GET");
HttpURLConnection.setFollowRedirects(true);
huc.connect();
final int code = huc.getResponseCode();
String message = huc.getResponseMessage();
Turns out I needed to activate custom messages on Tomcat using this parameter:
-Dorg.apache.coyote.USE_CUSTOM_STATUS_MSG_IN_HEADER=true
The message can be in the body rather than in header. Similar to a successful method, set the response (text, json, xml..) to be returned, but set the http status to an error value. I have found that to be more useful than the custom message in header. The following example shows the response with a custom header and a message in body. A ModelAndView that take to another page will also be conceptually similar.
#ExceptionHandler(InstanceNotFoundException.class)
public ResponseEntity<String> handle() {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("ACustomHttpHeader", "The custom value");
return new ResponseEntity<String>("the error message", responseHeaders, HttpStatus.INTERNAL_SERVER_ERROR);
}