Closed cazacmarin closed 2 years ago
@cazacmarin In sormas-rest we are so far using the ExceptionMapper of jax-rs (https://dennis-xlc.gitbooks.io/restful-java-with-jax-rs-2-0-2rd-edition/content/en/part1/chapter7/exception_handling.html). My first thought on this is that it would make more sense to go on with that approach instead of introducing a spring mechanism. Do you think it would be better to go with the approach you have described above? If so, why?
A small notice on the I18n stuff: I'd say we should use the existing approach based on I18nProperties. Maybe use an enum for the different error codes you mentioned.
I did some tests, in order to have a clean answer to right approach.
General idea is to create builders. And those builders, will populate data from exception accordingly to error code and other details at the moment when exception is thrown.
In order to implement it, is needed that all controllers will extend from CustomizedExceptionHandler
class witch should extend org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
class.
1st and main problem which I saw: if, we are extending from javax.ws.rs.WebApplicationException
in the manner which I described, call to the controller (any methods) will lead to 500 server error with general exception of injection. This one I cannot bypass in any manner.
2nd important problem is the generated output. We definitely need to replace javax.ws.rs.core.Response
to org.springframework.http.ResponseEntity
. Content generated by jax-rs library, contain much more details than needed. And builder, will be represented in toString
format within section reason_phrase
. It means that client side need to parse this exception, to extract from it details from toString
method which can change in time. (not a good practice).
Output generated files are the next one:
response-with-javax.ws.rs.core.Response.json.log
response-with-org.springframework.http.ResponseEntity.json.log
3rd At mapping exception with jax-rs, sometime we can have mapping exception. And only general message will be thrown. Builder message itself, will not be created. In order to fix it, additional library should be added(and this one needs to be tested to be sure that output exception was created by builder):
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-common</artifactId>
<version>2.22.2</version>
<scope>compile</scope>
</dependency>
Based on that, I can conclude that we need to proceed with spring library.
Due to the way how sormas-rest
is created and Swagger app how is configured and registering controllers, spring library cannot be used.
Finally, I was able to do it with jax-rs. After a lot of issues, solution is the next one:
sormas-api
shared module will be used as an intermediate for accessing sormas-rest
and sormas-backend
project.sormas-api
will contain entire custom CustomizedException
mechanism.sormas-rest
and sormas-backend
)I created the patch as a proof of concept and added it here: (annotations and inheritance as is, are very important to be there, otherwise, something will not work. #8934_-_Build_custom_Error_Details_message_for_REST_API.zip
Can be used as well translation from server side.
Output, looks like next one:
ArgumetList var ca be used to customize some exception.
Afterword, if all is agreed, module by module tickets ca be opened and this mechanism can be applied for entire rest-api
module.
(moving ticket into wait column as Martin input is required on it. When we will agree that is is good to be implemented, all relevant tickets will be opened for rest APIs and this ticket will be closed.)
Thanks for diving into this. I looked into your new proposal and the patch and have a few comments:
All in all I think what we are still missing here is to take a step back to get an overview and define the following:
@vidi42 @JonasCir I'd also like your input on this. Probably worth to setup a meeting with the 4 of us once you have had a look at this. Maybe also Fredrik?
Hi @MartinWahnschaffe. Some answers to your questions:
2.I don't get when we would need the ExceptionDto with the id and the arguments list. Do you have an example for this?
[Marin]arguments list is used to pass some additional variable to front. In case of angular if translation will be on the front side of app.
in case if translation will be only on backend side, than this list is not needed.
5. I think we should map those error messages to http status codes
[Marin] - generally speaking, yes. But sometime can exist scenarios in which are required to send a specific error code. So in my opinion both options should be supported.6. Instead of using string constants in the ExceptionsTemplate class I'd suggest to use an enum class and thus also provide I18n
[Marin] - make sense, 2nd commit contain this implementation.Where are those authentication related constants like TOKEN_IS_NOT_VALID_MSG used? We probably need an exception mapper for authentication related exceptions, right?
[Marin]- Yes, that's true. I will try to implement it as well.All in all I think what we are still missing here is to take a step back to get an overview and define the following:
1. Which exceptions are thrown from the backend and rest application?
[Marin] - In my opinion is good if we can do all on backend side. In case of error modiffication, will be much more easy if we will modify the exception only on backend side.
2. Which exceptions are missing and should be thrown? E.g. *Resource.getByUuid could throw an "EntityNotFoundException" or maybe a similar exception that already exists.
[Marin] - NEED to find all the list from them.
3. For each of those exceptions we should define how they are mapped. Currently we only have a mapper for ConstraintViolationException and EJBAccessExceptionMapper
[Marin] - usually this can be progressively done 1 by 1 when this mechanism will be implemented and all relevant tickets will be opened
4. This mapping could rely on an enum that defines all the potential ReST error results, including HTTP status code and an I18n message.
[Marin] - Totally agreed. I will think about it's implementation as well.
2nd commit error message. In case if additional fields are still required, please let here a note.
Related to the 2. in the definition need pointed out by @MartinWahnschaffe , I would like to make a start.
When pushing invalid entities, users sometimes only get a 400 Bad Request
returned. When you don't have access to the SORMAS server logs, this makes it very hard/impossible to see the cause of the problem. I think it would be very helpful, if instead an explanation of what's wrong with the pushed entity gets returned, so that the sender can find out what to fix.
For some scenarios, this is already done (e.g. when not sending some mandatory attributes).
When a sending user does not have sufficient rights, currently only something rather generic gets returned (like 403 Forbidden
). Maybe it would help to point out what right is missing? Like that, it would also be easier to distinguish user rights issues from server configuration issues, which might also lead to 403s.
In general I think we should get rid of our custom approach and stick with exception mappers. They are quite flexible and directly integrated into the stack.
Based on https://stackoverflow.com/questions/3293599/jax-rs-using-exception-mappers
- it is possible to map exception builders.
@JonasCir - if you can, it is better to make a PoC as we will be able to compare which solution is better.
In link provided, there were a lot of if/else statement, and not a normal, parametrized, with different arguments and if needed with internationalization thrown error.
When you will have a nicely solution as was provided by custom approach which can easy extend and have the same message format
for all of the exceptions, please leave here a note, and I will be glad to take a look on it :)
Thanks!
We can get rid of the if-else chains as jax will go through the super classes of an exception to find a matching handler, so we can start generic and then specialize if necessary. I think we can enforce a common scheme. For example, there is https://www.rfc-editor.org/rfc/rfc7807 which could help. I guess there will be a library or we can define a response entity class of a reasonable format which we serialize.
Agree. All is possible :) - it is matter of time for new investigation. Due to that, I asked to have a branch with a new proposal
, and only after, we can raise all pros
and cons
of both solutions. And only after, we can decide which one is the best one. And win
solution will be followed. Hope it make sense for everybody :)
Dropping this in favor of #10251
Situation Description
Currently, Sormas Rest API is handling a small range of errors which can be reported to the front(or service which is interrogating REST endpoints). If appear a scenario where a new specific error needs to be added, it is done in if/else block and those blocks will increase is size. Moreover, they contain only a specific text message.
Feature Description
General epic where this feature can be added is: #7408
As AlexV suggested, more description is inside:
All rest controllers/resources need to extend from extends CustomizedExceptionHandler
And CustomizedExceptionHandler need to extends from public abstract class ResponseEntityExceptionHandler
In Sormas-Rest next dependency should be added:
This dependency is linked to
ResponseEntityExceptionHandler
classCustomizedExceptionHandler
class Will contain all builders for all exceptions. Some examples:@ExceptionHandler(TokenIsNotValidException.class) public ResponseEntity handleTokenErrors(
TokenIsNotValidException exception) {
}
@ExceptionHandler(EntityNotFoundException.class) public ResponseEntity handleEntityErrors(
EntityNotFoundException exception) {
}
@ExceptionHandler(UsernameNotFoundOrNoLongerActiveException.class) public ResponseEntity handleUsernameNotFoundOrNoLongerActiveErrors(
UsernameNotFoundOrNoLongerActiveException exception) {
}
@ExceptionHandler(UserUnauthorizedException.class) public ResponseEntity handleUserErrors(
UserUnauthorizedException exception) {
}
@ExceptionHandler(SameEntityExistsException.class) public ResponseEntity handleSameEntityExistsErrors(
SameEntityExistsException exception) {
}
public final static String TOKEN_IS_NOT_VALID_MSG = "Token is not valid"; public final static String RECORD_NOT_FOUND_MSG = "Record not found"; public final static String RECORD_ALREADY_EXISTS = "Record already exists"; public final static String PASS_NOT_MATCHING = "Password not match";
public static Map<String, ExceptionDao> errorMap = new HashMap<>();
public void populateExceptionMap() { errorMap.put( TOKEN_IS_NOT_VALID_MSG, new ExceptionDao( 1, new ArrayList<>() ) ); errorMap.put( RECORD_NOT_FOUND_MSG, new ExceptionDao( 2, new ArrayList<>() ) ); errorMap.put( RECORD_ALREADY_EXISTS, new ExceptionDao( 3, new ArrayList<>() ) ); errorMap.put( PASS_NOT_MATCHING, new ExceptionDao( 4, new ArrayList<>() ) );
RoleEntity re = roleRepo.findByDefaultRole( String.valueOf( DefaultRoleName._USER ) ) .orElseThrow( () -> new EntityNotFoundException( RoleEntity.class, String.valueOf( DefaultRoleName._USER ) ) );
RecoveryTokenEntity dbRecoveryToken = recoveryTokenRepo.findOneByUserId( userId ) .orElseThrow( () -> new TokenIsNotValidException( RecoveryTokenEntity.class, resetUserPasswordDto.getRecoveryToken().toString() ) );
if (userRepo.findByEmailIgnoreCase( newUserDto.getEmail() ).isPresent()) { throw new SameEntityExistsException( UserEntity.class, newUserDto.getEmail() ); }
if (!authUtils.matchesPassword( updatableUserPasswordDto.getOldPassword(), user.getPassword() )) { throw new BadCredentialsException( UserEntity.class, USER_PASSWORDS_MISMATCH_OLD_CURRENT ); }
Optional userEntityOptional = userRepo.findByEmailIgnoreCase( email );
if (userEntityOptional.isEmpty() || userEntityOptional.get().getUserStatus() == UserStatus.INACTIVE) {
log.info( "attempt to login with invalid or inactive username: {}", email );
throw new UsernameNotFoundOrNoLongerActiveException( UserEntity.class, USERNAME_NOT_FOUND_OR_NO_LONGER_ACTIVE );
}