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 {
}
Related
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)))
Not able to set HTTP Status code for a custom ThingWorx service.
I need to send appropriate HTTP status code for error conditions in my Thingworx service. When I use GenericHTTPException, it sets the right code, but it prepends "Unable to Invoke Service" to my JSON response and that does not work for the AJAX client. I should be able to send pure JSON response along with the right HTTP status code
#ThingworxServiceDefinition(name = "GetServiceProviderHeirarchy", category = "PTC")
#ThingworxServiceResult(name = "result", baseType = "JSON")
public JSON GetServiceProviderHeirarchy(
#ThingworxServiceParameter(name = "seedURI", baseType = "STRING")String seedURI,
#ThingworxServiceParameter(name = "depth", baseType = "INTEGER")Integer depth,
#ThingworxServiceParameter(name = "resourceType", baseType = "STRING")String resourceType,
#ThingworxServiceParameter(name = "serverName", baseType = "STRING")String serverName)
throws Exception {
if(serverName == null || serverName.isEmpty()){
JSONObject jsonErrObject = new JSONObject();
jsonErrObject.put("message", "Values to input parameter serverName is missing");
logger.error("serverName is not provided");
throw new InvalidRequestException(jsonErrObject.toString(), RESTAPIConstants.StatusCode.STATUS_BAD_REQUEST);
}
}
Actual result:
Unable to Invoke Service GetServiceProviderHeirarchy on
UpstreamOslcDataServicesThing : {"message":"No Configuration found for
resource type RequirementResourc. Please contact your administrator."}
Expected result:
{"message":"No Configuration found for resource type
RequirementResourc. Please contact your administrator."}
One possible option would be to include the jax-rs libraries as a ThingWorx resource and throw those specific exceptions as required. Say for bad request you can throw javax.ws.rs.BadRequestException'.
if(serverName == null || serverName.isEmpty()){
String message = "No Configuration found for resource. Please contact your administrator.";
throw new BadRequestException(message);
}
I'm using spring's #RepositoryRestResource annotation on a PagingAndSortingRepository.
When I send an erroneous payload to the corresponding endpoint, the error responses that are sent back are hard to parse, e.g.
{
"cause": {
"cause": {
"cause": null,
"message": "ERROR: duplicate key value violates unique constraint \"uk_bawli8xm92f30ei6x9p3h8eju\"\n Detail: Key (email)=(jhunstone0#netlog.com) already exists."
},
"message": "could not execute statement"
},
"message": "could not execute statement; SQL [n/a]; constraint [uk_bawli8xm92f30ei6x9p3h8eju]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement"
}
Is there any way to configure the messages, so it is clear which field (here: email) caused the error?
Regarding the error handling - you can implement a custom exception handler for such exceptions, extract the constraint name from the root cause, analyze it and create a corresponding message for the user.
Some error handling examples: 1, 2.
UPDATED
You should check the app log to determine which exception you have to handle. If I'm not mistaken for constraint violation we must handle org.springframework.dao.DataIntegrityViolationException, for example:
#ControllerAdvice
public class CommonExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(DataIntegrityViolationException.class)
ResponseEntity<?> handleDataIntegrityViolation(DataIntegrityViolationException ex, HttpServletRequest req) {
String causeMessage = NestedExceptionUtils.getMostSpecificCause(ex).getMessage(); // determine the root cause message
String reqPath = req.getServletPath(); // get the request path
String userMessage = ... // Decide what the message you will show to users
HttpStatus status = HttpStatus... // Decide what the status your response will be have, for example HttpStatus.CONFLICT
ApiErrorMessage message = new ApiErrorMessage(userMessage, status, reqPath); // Create your custom error message
return new ResponseEntity<>(message, status); // return response to users
}
// other handlers
}
Or you can implement this handler easier as in the official example.
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);
}
I have a ServiceStack REST service (PUT and POST) which I have tested with fiddler and if no errors are raised I return
new HttpResult(HttpStatusCode.OK);
Now I am testing the same REST service with the service stack client, I have:
var client = new XmlServiceClient("url"));
client.Post<ChangeServerLicenseDto>("", new ChangeServerLicenseDto()
{ServerName = model.ServerName});
and I get the exception on the REST service when I do
return new HttpResult(HttpStatusCode.OK)
and the error raised is :
500 (Error in line 1 position 76. Expecting element 'ChangeServerLicense'
from namespace ''.. Encountered 'Element' with name 'HttpStatusCode',
namespace 'http://schemas.datacontract.org/2004/07/System.Net'.)
My client code is in a MVC action method (POST).
My datacontract for the RestService is :
[DataContract(Name = "ChangeServerLicense", Namespace = "")]
[RestService("url", "POST", "application/xml")]
public class ChangeServerLicenseDto
{
[DataMember(Name = "ServerName", Order = 1)]
public string ServerName { get; set; }
}
The convention of signalling a successful response is to return an empty Response DTO (which by default returns a 200 OK). Also Send<TResponse>(...) does a POST so if you don't want to include the url in the request, use Send which will POST the request to the automatic pre-defined routes:
var client = new XmlServiceClient("url"));
client.Send<ChangeServerLicenseDtoResponse>(
new ChangeServerLicenseDto {ServerName = model.ServerName});
Otherwise if you still want to use .Post<T>(...) include the URL for the custom route where your services is mounted.
Note: I generally dislike using Dto suffixes on DTOs which are the most important API in your service - I explain in a bit more detail why here.