ExceptionHandler.java
/*
* Copyright 2016-2017 Providence Authors
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package net.morimekta.providence.server;
import net.morimekta.providence.PApplicationException;
import net.morimekta.providence.PMessageOrBuilder;
import net.morimekta.providence.PServiceCall;
import net.morimekta.providence.PServiceCallHandler;
import net.morimekta.providence.descriptor.PService;
import net.morimekta.providence.serializer.Serializer;
import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static net.morimekta.providence.PApplicationExceptionType.BAD_SEQUENCE_ID;
import static net.morimekta.providence.PApplicationExceptionType.INVALID_MESSAGE_TYPE;
import static net.morimekta.providence.PApplicationExceptionType.INVALID_PROTOCOL;
import static net.morimekta.providence.PApplicationExceptionType.PROTOCOL_ERROR;
import static net.morimekta.providence.PApplicationExceptionType.UNKNOWN_METHOD;
/**
* Handle exceptions for HTTP responses. Note that using this instead of a proper
* service handler will easily hide exceptions, or transform unknown exceptions into
* known exceptions outside of service implementations. But it provides a handy
* way of normalizing non-default responses onto HTTP status and message format.
*
* <h2>Overridable Methods</h2>
*
* <ul>
* <li>
* <code>{@link #writeErrorResponse(Throwable, Serializer, HttpServletRequest, HttpServletResponse)}</code>:
* Complete handling of exceptions thrown by the {@link PServiceCallHandler#handleCall(PServiceCall, PService)}
* method or similar. Calling super on this will fall back to default exception handling, using the methods
* below. This will per default serialize PMessage exceptions normally, and just call
* {@link HttpServletResponse#sendError(int,String)} for all the others using the {@link Exception#getMessage()}
* message.
* </li>
* <li>
* <code>{@link #getResponseException(Throwable)}</code>: Get the response exception given the specific
* thrown exception. This method can be used to unwrap wrapped exceptions, or transform non- message
* exceptions into providence message exceptions.
* </li>
* <li>
* <code>{@link #statusCodeForException(Throwable)}</code>: Get the HTTP status code to be used for the
* error response. Override to specialize, and call super to get default behavior. The default will
* handle {@link PApplicationException} errors, and otherwise return <code>500 Internal Server Error</code>.
* </li>
* </ul>
*/
public class ExceptionHandler {
public static final ExceptionHandler INSTANCE = new ExceptionHandler();
public ExceptionHandler() {}
/**
* Handle exceptions from the handle method. This method can be overridden if
* a thrown exception must be handled <b>before</b> it is transformed with
* {@link #getResponseException(Throwable)}. Otherwise override
* {@link #writeErrorResponse(Throwable, Serializer, HttpServletRequest, HttpServletResponse)}
* instead.
*
* @param rex The response exception, which is the thrown exception or one
* of it's causes. See {@link #getResponseException(Throwable)}.
* @param responseSerializer The serializer to use to serialize message output.
* @param httpRequest The HTTP request.
* @param httpResponse The HTTP response.
* @throws IOException If writing the response failed.
*/
public void handleException(
@Nonnull Throwable rex,
@Nonnull Serializer responseSerializer,
@Nonnull HttpServletRequest httpRequest,
@Nonnull HttpServletResponse httpResponse) throws IOException {
rex = getResponseException(rex);
writeErrorResponse(rex, responseSerializer, httpRequest, httpResponse);
}
/**
* Write the error response. Override this method in order to write the
* response or response headers specifically. Callers must call super
* if response not written. It is fine to set headers and call super too.
*
* @param rex The response exception.
* @param responseSerializer The response serializer.
* @param httpRequest The request.
* @param httpResponse The response.
* @throws IOException If writing the response failed.
*/
protected void writeErrorResponse(@Nonnull Throwable rex,
@Nonnull Serializer responseSerializer,
@Nonnull HttpServletRequest httpRequest,
@Nonnull HttpServletResponse httpResponse) throws IOException {
if (!httpResponse.isCommitted()) {
if (rex instanceof PMessageOrBuilder) {
PMessageOrBuilder<?> mex = (PMessageOrBuilder) rex;
httpResponse.setStatus(statusCodeForException(rex));
httpResponse.setContentType(responseSerializer.mediaType());
responseSerializer.serialize(httpResponse.getOutputStream(), mex);
} else {
int code = statusCodeForException(rex);
ProvidenceHttpError mex = ProvidenceHttpError
.builder()
.setMessage(rex.getMessage())
.setStatusCode(code)
.build();
httpResponse.setStatus(code);
httpResponse.setContentType(responseSerializer.mediaType());
responseSerializer.serialize(httpResponse.getOutputStream(), mex);
}
}
httpResponse.flushBuffer();
}
/**
* Get the exception to ge handled on failed requests.
*
* @param e The exception seen.
* @return The exception to use as response base.
*/
@Nonnull
protected Throwable getResponseException(Throwable e) {
return e;
}
/**
* With default exception handling, this can simply change the status code used
* for the response.
*
* @param exception The exception seen.
* @return The status code to be used.
*/
protected int statusCodeForException(@Nonnull Throwable exception) {
if (exception instanceof PApplicationException) {
PApplicationException e = (PApplicationException) exception;
if (e.getType() == INVALID_PROTOCOL ||
e.getType() == PROTOCOL_ERROR ||
e.getType() == BAD_SEQUENCE_ID ||
e.getType() == INVALID_MESSAGE_TYPE) {
return HttpServletResponse.SC_BAD_REQUEST;
}
if (e.getType() == UNKNOWN_METHOD) {
return HttpServletResponse.SC_NOT_FOUND;
}
}
return HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
}
}