Skip to content

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

DaniilRoman
Copy link

@DaniilRoman DaniilRoman commented Mar 15, 2025

#608

Problem: Polaris Server exposes backend meta in error response body when database is no bootstraped
Solution: Create a separated jakarta.persistence.PersistenceException mapper

Copy link
Contributor

@flyrain flyrain left a 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();
Copy link
Contributor

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);
    }

Copy link
Author

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();
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Author

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

@DaniilRoman DaniilRoman changed the title Add the PersistenceException handler in IcebergExceptionMapper Create PersistenceException mapper Mar 20, 2025

@Provider
public class IcebergPersistenceExceptionMapper
implements ExceptionMapper<PersistenceException> {
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Author

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

Copy link
Contributor

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())
Copy link
Contributor

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.

Copy link
Contributor

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

Copy link
Contributor

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? :)

Copy link
Contributor

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())
Copy link
Contributor

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?

Copy link
Author

@DaniilRoman DaniilRoman Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is absolutely right, my bad 🤦
I'm making it correct by using ExceptionUtils.getRootCause

I can reproduce it in PolarisEclipseLinkMetaStoreManagerTest.java by messing with persistence.xml
Screenshot 2025-03-21 at 23 04 06

Copy link
Contributor

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?

Copy link
Contributor

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.

.

In that case, we don't have to make any change to mappers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM ^

@flyrain flyrain closed this Apr 30, 2025
@github-project-automation github-project-automation bot moved this from PRs In Progress to Done in Basic Kanban Board Apr 30, 2025
@flyrain flyrain reopened this Apr 30, 2025
@github-project-automation github-project-automation bot moved this from Done to PRs In Progress in Basic Kanban Board Apr 30, 2025
@flyrain
Copy link
Contributor

flyrain commented Apr 30, 2025

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.

@DaniilRoman
Copy link
Author

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 🙏

@flyrain
Copy link
Contributor

flyrain commented May 22, 2025

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants