JAX-RS: capture all error, don't expose internal stack trace


The Goal: Don't expose internal stack trace to client

Capture all Error
When we are developing restful API that are exposed to the internet,  we better capture all exception/error, and don't expose internal stack trace to outside.

In Jersey, we can use ExceptionMapper to capture any kind of exception or error.
For known exception such as ProductNotFound, we can capture it, and return meaningful error message to client.

But bad things always happen, our application may throw NullPointerExceptionn or exception that we don't ever expect or may throw OutOfMemoryError.

When this happens, we need capture it, return some message like: "internal error", and then asynchronously notify engineer owners(send email, store to db or or other approaches).

public class GenericExceptionMapper implements ExceptionMapper {
    private static final Logger LOGGER = LoggerFactory.getLogger(GenericExceptionMapper.class);
    private static final String MSG_INTERNAL_ERROR = "internal error.";
    public static final String HEADER_ERROR_CODE = "X-ErrorCode";

    @Override
    public Response toResponse(Throwable exception) {
        // including exception's type and message in the log message to facilitate log navigation

        // here we capture all error, so no stack trace is exposed to client.
        // TODO alert developers
        LOGGER.error("Unhanded exception detected {}:{}",
                new Object[] {exception.getClass(), exception.getMessage(), exception});

        if (exception instanceof OutOfMemoryError) {
            // notify engineer owner
        } else {
            // may caused by program bug
            // store the error to separate log or db, so we can check them easily
        }

        ResponseBuilder builder = Response.status(Response.Status.INTERNAL_SERVER_ERROR);
        builder.header(HEADER_ERROR_CODE, MSG_INTERNAL_ERROR);
        builder.entity(new ErrorMessage(MSG_INTERNAL_ERROR)).type(MediaType.APPLICATION_JSON);
        return builder.build();
    }
}

Code is the King
How Jersey find exception mapper for specific exception?
com.sun.jersey.spi.container.ContainerResponse.mapException(Throwable)
com.sun.jersey.server.impl.application.ExceptionMapperFactory.find(Class)

It checks all exception mappers, get the exception mapper whose exception type is isAssignableFrom and the distance from current exception is smallest.

If can't find. it will ResponseListener onError, which by default returns html page with error stack trace.

    public ExceptionMapper find(Class c) {
        int distance = Integer.MAX_VALUE;
        ExceptionMapper selectedEm = null;
        for (ExceptionMapperType emt : emts) {
            int d = distance(c, emt.c);
            if (d < distance) { 
                distance = d;
                selectedEm = emt.em;
                if (distance == 0) break;
            }
        }
        
        return selectedEm;
    }
    
    private int distance(Class c, Class emtc) {
        int distance = 0;
        if (!emtc.isAssignableFrom(c))
            return Integer.MAX_VALUE;
        
        while (c != emtc) {
            c = c.getSuperclass();
            distance++;
        }
        
        return distance;
    }

Labels

adsense (5) Algorithm (69) Algorithm Series (35) Android (7) ANT (6) bat (8) Big Data (7) Blogger (14) Bugs (6) Cache (5) Chrome (19) Code Example (29) Code Quality (7) Coding Skills (5) Database (7) Debug (16) Design (5) Dev Tips (63) Eclipse (32) Git (5) Google (33) Guava (7) How to (9) Http Client (8) IDE (7) Interview (88) J2EE (13) J2SE (49) Java (186) JavaScript (27) JSON (7) Learning code (9) Lesson Learned (6) Linux (26) Lucene-Solr (112) Mac (10) Maven (8) Network (9) Nutch2 (18) Performance (9) PowerShell (11) Problem Solving (11) Programmer Skills (6) regex (5) Scala (6) Security (9) Soft Skills (38) Spring (22) System Design (11) Testing (7) Text Mining (14) Tips (17) Tools (24) Troubleshooting (29) UIMA (9) Web Development (19) Windows (21) xml (5)