-
Notifications
You must be signed in to change notification settings - Fork 245
Create PersistenceException mapper #1181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Create PersistenceException mapper #1181
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @DaniilRoman for working on a fix. Left some comments.
@@ -191,6 +192,7 @@ static int mapExceptionToResponseCode(RuntimeException rex) { | |||
case IllegalArgumentException e -> Status.BAD_REQUEST.getStatusCode(); | |||
case UnsupportedOperationException e -> Status.NOT_ACCEPTABLE.getStatusCode(); | |||
case WebApplicationException e -> e.getResponse().getStatus(); | |||
case PersistenceException e -> Status.UNAUTHORIZED.getStatusCode(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may not be enough to fix the issue, as line 105 to line 115 still leak the detailed database information.
Another concern is that, the error log has gone after the change as we changed the Status code
to 401. I think we still need to log an error in the server side.
if (responseCode == Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) {
LOGGER.error("Unhandled exception returning INTERNAL_SERVER_ERROR", runtimeException);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your feedback 🙏
This makes total sense. Instead of extending the existing mapper, I've created a new one where I set a message from PersistenceException but shouldn't include the information like Exception [EclipseLink-4002] (Eclipse Persistence Services - 4.0.4.v202407190748...
I've checked existing mappers and I see that we use a level INFO for every known exception I've chosen an INFO level as well instead of an ERROR level
@@ -191,6 +192,7 @@ static int mapExceptionToResponseCode(RuntimeException rex) { | |||
case IllegalArgumentException e -> Status.BAD_REQUEST.getStatusCode(); | |||
case UnsupportedOperationException e -> Status.NOT_ACCEPTABLE.getStatusCode(); | |||
case WebApplicationException e -> e.getResponse().getStatus(); | |||
case PersistenceException e -> Status.UNAUTHORIZED.getStatusCode(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the rationale for mapping PersistenceException
to UNAUTHORIZED
? Are we sure all of those errors are caused by failed authorization checks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, looks like 500 is more suitable for PersistenceException
. The 401 status code is reserved for authentication failures, which isn't applicable here. We may only need to mask the sensitive information a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your feedback 🙏
I've adjusted this PR and changed the error code back to 500
|
||
@Provider | ||
public class IcebergPersistenceExceptionMapper | ||
implements ExceptionMapper<PersistenceException> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure creating a separate mapper just for PersistenceException
is a good idea. IcebergExceptionMapper
already deals with exception of multiple types. Could you integrate the new logic for PersistenceException
there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a right thing to use a different class, instead of putting it into IcebergExceptionMapper
. I even think we may pull non-iceberg exception out of the class IcebergExceptionMapper
, whose name indicates that it handles iceberg exceptions, but not other exceptions.
I'd suggest to rename it to PersistenceExceptionMapper
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also think it's reasonable to create a separate mapper for PersistenceException. In IcebergExceptionMapper
we treat any "500" exceptions like "unhandled" and I would need to create yet another "if else" for PersistenceException which would make IcebergExceptionMapper even more complicated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the web layer exceptions may be wrapped in RuntimeException
and other generic exception classes. There's usually logic to unwrap the root cause and process it depending on the specific type. Also, there's usually logic for logging interesting exceptions.
If more than one ExceptionMapper
is used, provisions should be made to apply the same processing logic in all cases.
However, in this PR the exception processing logic is forked.
ErrorResponse icebergErrorResponse = | ||
ErrorResponse.builder() | ||
.responseCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) | ||
.withType(exception.getClass().getSimpleName()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This "type" is supposed to be interpreted by Iceberg REST API clients, but I do not think "PersistenceException" is going to be very meaningful on the client side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like the class IcebergExceptionMapper
also use the exception simple name as the type. I'm also not sure if any iceberg rest client relies on the type. I think the status code is good enough for most of use cases. Here is the error message definition in Iceberg REST spec. Looks like the type isn't tied to the status code. I think it's OK to use the exception name, but open to suggestions.
ErrorModel:
type: object
description: JSON error payload returned in a response with further details on the error
required:
- message
- type
- code
properties:
message:
type: string
description: Human-readable error message
type:
type: string
description: Internal type definition of the error
example: NoSuchNamespaceException
code:
type: integer
minimum: 400
maximum: 600
description: HTTP response code
example: 404
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Existing does not mean correct, eh? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it's better than nothing... ideally, these exceptions should be caught and wrapped within the business logic before making it back to the client. They would be wrapped with something that the IcebergExceptionMapper or PolarisExceptionMapper can map to a client-friendly response. However in the event that we miss the wrapping, I can see how this additional mapper is useful.
ErrorResponse.builder() | ||
.responseCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) | ||
.withType(exception.getClass().getSimpleName()) | ||
.withMessage(exception.getMessage()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description mentions avoiding exposing internal database information in these errors, but will this message not contain database-specific information?
Do you have an example exception and corresponding JSON error response?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can we be sure that ExceptionUtils.getRootCause().getMessage()
(current code) does not expose internal database information?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this moment, I'd suggest to wrap the jakarta.persistence.PersistenceException
in EclipseLink module in a RuntimeException, like what we did in JDBC impl.
Line 98 in d3b24d2
throw new RuntimeException("Error persisting entity", e); |
In that case, we don't have to make any change to mappers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM ^
Hi @DaniilRoman, thanks a lot for the contribution. Let me know if you are still working on it, or this can be picked up by others. |
Hi @flyrain 👋 My apologies for such a late reply. Unfortunately, I don't have any time at the moment, and I'd appreciate it if anyone would contribute to this solution 🙏 |
No worries. Thanks for the contributions! |
#608
Problem: Polaris Server exposes backend meta in error response body when database is no bootstraped
Solution: Create a separated jakarta.persistence.PersistenceException mapper