All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.nike.riposte.server.handler.ExceptionHandlingHandler Maven / Gradle / Ivy

There is a newer version: 0.20.0
Show newest version
package com.nike.riposte.server.handler;

import com.nike.riposte.server.channelpipeline.ChannelAttributes;
import com.nike.riposte.server.error.exception.IncompleteHttpCallTimeoutException;
import com.nike.riposte.server.error.exception.InvalidRipostePipelineException;
import com.nike.riposte.server.error.exception.TooManyOpenChannelsException;
import com.nike.riposte.server.error.handler.ErrorResponseBody;
import com.nike.riposte.server.error.handler.ErrorResponseInfo;
import com.nike.riposte.server.error.handler.RiposteErrorHandler;
import com.nike.riposte.server.error.handler.RiposteUnhandledErrorHandler;
import com.nike.riposte.server.handler.base.BaseInboundHandlerWithTracingAndMdcSupport;
import com.nike.riposte.server.handler.base.PipelineContinuationBehavior;
import com.nike.riposte.server.http.Endpoint;
import com.nike.riposte.server.http.HttpProcessingState;
import com.nike.riposte.server.http.ProxyRouterEndpoint;
import com.nike.riposte.server.http.ProxyRouterProcessingState;
import com.nike.riposte.server.http.RequestInfo;
import com.nike.riposte.server.http.ResponseInfo;
import com.nike.riposte.server.http.impl.FullResponseInfo;
import com.nike.riposte.server.http.impl.RequestInfoImpl;

import com.fasterxml.jackson.core.JsonProcessingException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Map;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpRequest;

import static com.nike.riposte.server.channelpipeline.ChannelAttributes.getProxyRouterProcessingStateForChannel;
import static com.nike.riposte.server.handler.base.BaseInboundHandlerWithTracingAndMdcSupport.HandlerMethodToExecute.DO_EXCEPTION_CAUGHT;
import static com.nike.riposte.util.AsyncNettyHelper.callableWithTracingAndMdc;

/**
 * Handles errors thrown due to the incoming message and converts them to the appropriate {@link ResponseInfo} payload
 * so it can be sent to the user. Also catches cases where an error was not thrown but the request somehow slipped
 * through our endpoints without being handled at all, and sets up an error payload so that the response sending handler
 * spits out an error (since this case indicates a pipeline misconfiguration).
 * 

* This handler should come directly before {@link ResponseSenderHandler} in the pipeline so that we don't have handlers * between this class and the response sender that could throw errors that don't get handled properly. It should also * come after the main request handlers (e.g. {@link NonblockingEndpointExecutionHandler} and {@link * ProxyRouterEndpointExecutionHandler}) to give the request a chance to be handled at all. * * @author Nic Munroe */ @SuppressWarnings("WeakerAccess") public class ExceptionHandlingHandler extends BaseInboundHandlerWithTracingAndMdcSupport { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final RiposteErrorHandler riposteErrorHandler; private final RiposteUnhandledErrorHandler riposteUnhandledErrorHandler; public ExceptionHandlingHandler(RiposteErrorHandler riposteErrorHandler, RiposteUnhandledErrorHandler riposteUnhandledErrorHandler) { if (riposteErrorHandler == null) throw new IllegalArgumentException("riposteErrorHandler cannot be null"); if (riposteUnhandledErrorHandler == null) throw new IllegalArgumentException("riposteUnhandledErrorHandler cannot be null"); this.riposteErrorHandler = riposteErrorHandler; this.riposteUnhandledErrorHandler = riposteUnhandledErrorHandler; } @Override public PipelineContinuationBehavior doExceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // We expect to end up here when handlers previously in the pipeline throw an error, so do the normal // processError call. HttpProcessingState state = getStateAndCreateIfNeeded(ctx, cause); if (state.isResponseSendingStarted()) { String infoMessage = "A response has already been started. Ignoring this exception since it's secondary. NOTE: This often " + "occurs when an error happens repeatedly on multiple chunks of a request or response - only the " + "first one is processed into the error sent to the user. The original error is probably higher up in " + "the logs. ignored_secondary_exception=\"{}\""; if (cause instanceof NullPointerException) logger.info(infoMessage, cause.toString(), cause); else logger.info(infoMessage, cause.toString()); return PipelineContinuationBehavior.DO_NOT_FIRE_CONTINUE_EVENT; } else { ResponseInfo responseInfo = processError(state, null, cause); if (shouldForceConnectionCloseAfterResponseSent(cause)) responseInfo.setForceConnectionCloseAfterResponseSent(true); state.setResponseInfo(responseInfo); // We're about to send a full error response back to the original caller, so any proxy/router streaming is // invalid. Cancel request and response streaming for proxy/router endpoints. Endpoint endpoint = state.getEndpointForExecution(); if (endpoint != null && endpoint instanceof ProxyRouterEndpoint) { ProxyRouterProcessingState proxyRouterState = getProxyRouterProcessingStateForChannel(ctx).get(); if (proxyRouterState != null) { proxyRouterState.cancelRequestStreaming(cause, ctx); proxyRouterState.cancelDownstreamRequest(cause); } } } return PipelineContinuationBehavior.CONTINUE; } @Override public PipelineContinuationBehavior doChannelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // We expect to be here for normal message processing, but only as a pass-through. If the state indicates that // the request was not handled then that's a pipeline misconfiguration and we need to throw an error. HttpProcessingState state = getStateAndCreateIfNeeded(ctx, null); if (!state.isRequestHandled()) { callableWithTracingAndMdc(() -> { String errorMsg = "In ExceptionHandlingHandler's channelRead method, but the request has not yet been " + "handled. This should not be possible and indicates the pipeline is not set up " + "properly or some unknown and unexpected error state was triggered. Sending " + "unhandled error response"; logger.error(errorMsg); Exception ex = new InvalidRipostePipelineException(errorMsg); ResponseInfo responseInfo = processUnhandledError(state, msg, ex); state.setResponseInfo(responseInfo); return null; }, ctx).call(); } return PipelineContinuationBehavior.CONTINUE; } @Override protected boolean argsAreEligibleForLinkingAndUnlinkingDistributedTracingInfo( HandlerMethodToExecute methodToExecute, ChannelHandlerContext ctx, Object msgOrEvt, Throwable cause ) { return (methodToExecute == DO_EXCEPTION_CAUGHT); } protected HttpProcessingState getStateAndCreateIfNeeded(ChannelHandlerContext ctx, Throwable cause) { HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get(); if (state == null) { // The error must have occurred before RequestStateCleanerHandler could even execute. Create a new state and // put it into the channel so that we can populate the ResponseInfo for our response sender. logger.error( "No HttpProcessingState was available. This means the error occurred before RequestStateCleanerHandler " + "could execute.", cause ); state = new HttpProcessingState(); ctx.channel().attr(ChannelAttributes.HTTP_PROCESSING_STATE_ATTRIBUTE_KEY).set(state); } return state; } /** * Tries to extract the {@link RequestInfo} associated with the current request using the given arguments. First it * will try to get it from the given state. If that fails, it will try to create a new one based on the given msg * (which only works if the msg is a {@link HttpRequest}). If that also fails then a new dummy instance for an * unknown request will be created via {@link RequestInfoImpl#dummyInstanceForUnknownRequests()} and returned. */ RequestInfo getRequestInfo(HttpProcessingState state, Object msg) { // Try to get the RequestInfo from the state variable first. RequestInfo requestInfo = state.getRequestInfo(); // If the state did not have a request info, see if we can build one from the msg if (requestInfo == null && msg != null && (msg instanceof HttpRequest)) requestInfo = new RequestInfoImpl((HttpRequest) msg); // If requestInfo is still null then something major blew up, and we just need to create a dummy one for an // unknown request if (requestInfo == null) requestInfo = RequestInfoImpl.dummyInstanceForUnknownRequests(); return requestInfo; } /** * Attempts to process the given error using the "normal" error handler {@link #riposteErrorHandler} to produce the * most specific error response possible for the given error. If that fails for any reason then the unhandled error * handler will take over to guarantee the user gets a generic error response that still follows our error contract. * If you already know your error is a non-normal unhandled error of the "how did we get here, this should never * happen" variety you can (and should) directly call {@link #processUnhandledError(HttpProcessingState, Object, * Throwable)} instead. */ protected ResponseInfo processError(HttpProcessingState state, Object msg, Throwable cause) throws JsonProcessingException { RequestInfo requestInfo = getRequestInfo(state, msg); try { ErrorResponseInfo contentFromErrorHandler = riposteErrorHandler.maybeHandleError(cause, requestInfo); if (contentFromErrorHandler != null) { // The regular error handler did handle the error. Setup our ResponseInfo. ResponseInfo responseInfo = new FullResponseInfo<>(); setupResponseInfoBasedOnErrorResponseInfo(responseInfo, contentFromErrorHandler); return responseInfo; } } catch (Throwable errorHandlerFailed) { logger.error("An unexpected problem occurred while trying to handle an error.", errorHandlerFailed); } // If we reach here then it means the regular handler didn't handle the error (or blew up trying to handle it), // so the riposteUnhandledErrorHandler should take care of it. return processUnhandledError(state, msg, cause); } /** * Produces a generic error response. Call this if you know the error is a non-normal unhandled error of the "how * did we get here, this should never happen" variety, or if other attempts to deal with the error failed and you * need a guaranteed fallback that will produce a generic error response that follows our error contract. If you * have an error that happened during normal processing you should try {@link #processError(HttpProcessingState, * Object, Throwable)} instead in order to get an error response that is better tailored to the given error rather * than this one which guarantees a somewhat unhelpful generic error response. */ ResponseInfo processUnhandledError(HttpProcessingState state, Object msg, Throwable cause) throws JsonProcessingException { RequestInfo requestInfo = getRequestInfo(state, msg); // Run the error through the riposteUnhandledErrorHandler ErrorResponseInfo contentFromErrorHandler = riposteUnhandledErrorHandler.handleError(cause, requestInfo); ResponseInfo responseInfo = new FullResponseInfo<>(); setupResponseInfoBasedOnErrorResponseInfo(responseInfo, contentFromErrorHandler); return responseInfo; } protected void setupResponseInfoBasedOnErrorResponseInfo(ResponseInfo responseInfo, ErrorResponseInfo errorInfo) { responseInfo.setContentForFullResponse(errorInfo.getErrorResponseBody()); responseInfo.setHttpStatusCode(errorInfo.getErrorHttpStatusCode()); Map> extraHeaders = errorInfo.getExtraHeadersToAddToResponse(); if (extraHeaders != null) { for (Map.Entry> headerEntry : extraHeaders.entrySet()) { responseInfo.getHeaders().add(headerEntry.getKey(), headerEntry.getValue()); } } } protected boolean shouldForceConnectionCloseAfterResponseSent(Throwable cause) { return (cause instanceof TooManyOpenChannelsException) || (cause instanceof IncompleteHttpCallTimeoutException); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy