@Test public void constructor_works_with_valid_args() { // given ExceptionHandlingHandler handler = new ExceptionHandlingHandler( mock(RiposteErrorHandler.class), mock(RiposteUnhandledErrorHandler.class), mock(DistributedTracingConfig.class) ); // expect assertThat(handler, notNullValue()); }
HttpProcessingState state = getStateAndCreateIfNeeded(ctx, cause); if (state.isResponseSendingStarted()) { String infoMessage = ResponseInfo<ErrorResponseBody> responseInfo = processError(state, null, cause); if (shouldForceConnectionCloseAfterResponseSent(cause)) responseInfo.setForceConnectionCloseAfterResponseSent(true); addErrorAnnotationToOverallRequestSpan(state, responseInfo, cause);
/** * 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<ErrorResponseBody> processError(HttpProcessingState state, Object msg, Throwable cause) { 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<ErrorResponseBody> 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<ErrorResponseBody> processUnhandledError(HttpProcessingState state, Object msg, Throwable cause) { RequestInfo<?> requestInfo = getRequestInfo(state, msg); // Run the error through the riposteUnhandledErrorHandler ErrorResponseInfo contentFromErrorHandler = riposteUnhandledErrorHandler.handleError(cause, requestInfo); ResponseInfo<ErrorResponseBody> responseInfo = new FullResponseInfo<>(); setupResponseInfoBasedOnErrorResponseInfo(responseInfo, contentFromErrorHandler); return responseInfo; }
@Override public PipelineContinuationBehavior doChannelRead(ChannelHandlerContext ctx, Object msg) { // 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()) { runnableWithTracingAndMdc(() -> { 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<ErrorResponseBody> responseInfo = processUnhandledError(state, msg, ex); state.setResponseInfo(responseInfo, ex); addErrorAnnotationToOverallRequestSpan(state, responseInfo, ex); }, ctx).run(); } return PipelineContinuationBehavior.CONTINUE; }
@Test public void doExceptionCaught_should_cancel_proxy_router_processing_if_endpoint_is_ProxyRouterEndpoint() { // given ExceptionHandlingHandler handlerSpy = spy(handler); Throwable cause = new Exception("intentional test exception"); ResponseInfo<ErrorResponseBody> errorResponseMock = mock(ResponseInfo.class); doReturn(errorResponseMock).when(handlerSpy).processError(state, null, cause); assertThat(state.getResponseInfo(), nullValue()); // when PipelineContinuationBehavior result = handlerSpy.doExceptionCaught(ctxMock, cause); // then verify(handlerSpy).getStateAndCreateIfNeeded(ctxMock, cause); verify(handlerSpy).processError(state, null, cause); assertThat(state.getResponseInfo(), is(errorResponseMock)); Assertions.assertThat(state.getErrorThatTriggeredThisResponse()).isSameAs(cause); verify(handlerSpy).addErrorAnnotationToOverallRequestSpan(state, errorResponseMock, cause); assertThat(result, is(PipelineContinuationBehavior.CONTINUE)); }
@Test public void processError_gets_requestInfo_then_calls_riposteErrorHandler_then_converts_to_response_using_setupResponseInfoBasedOnErrorResponseInfo() throws UnexpectedMajorErrorHandlingError, JsonProcessingException { // given HttpProcessingState stateMock = mock(HttpProcessingState.class); Object msg = new Object(); Throwable cause = new Exception(); ExceptionHandlingHandler handlerSpy = spy(handler); RequestInfo<?> requestInfoMock = mock(RequestInfo.class); ErrorResponseInfo errorResponseInfoMock = mock(ErrorResponseInfo.class); RiposteErrorHandler riposteErrorHandlerMock = mock(RiposteErrorHandler.class); Whitebox.setInternalState(handlerSpy, "riposteErrorHandler", riposteErrorHandlerMock); doReturn(requestInfoMock).when(handlerSpy).getRequestInfo(stateMock, msg); doReturn(errorResponseInfoMock).when(riposteErrorHandlerMock).maybeHandleError(cause, requestInfoMock); // when ResponseInfo<ErrorResponseBody> response = handlerSpy.processError(stateMock, msg, cause); // then verify(handlerSpy).getRequestInfo(stateMock, msg); verify(riposteErrorHandlerMock).maybeHandleError(cause, requestInfoMock); ArgumentCaptor<ResponseInfo> responseInfoArgumentCaptor = ArgumentCaptor.forClass(ResponseInfo.class); verify(handlerSpy).setupResponseInfoBasedOnErrorResponseInfo(responseInfoArgumentCaptor.capture(), eq(errorResponseInfoMock)); ResponseInfo<ErrorResponseBody> responseInfoPassedIntoSetupMethod = responseInfoArgumentCaptor.getValue(); assertThat(response, is(responseInfoPassedIntoSetupMethod)); }
RequestInfo<?> requestInfo = exceptionHandlingHandler.getRequestInfo(state, msg); exceptionHandlingHandler.processUnhandledError(state, msg, exceptionToUse); responseSender.sendErrorResponse(ctx, requestInfo, responseInfo);
@UseDataProvider("exceptionsThatShouldForceCloseConnection") @Test public void doExceptionCaught_should_setForceConnectionCloseAfterResponseSent_to_true_on_request_when_exception_matches_certain_types( Throwable exThatShouldForceCloseConnection ) throws Exception { // given ExceptionHandlingHandler handlerSpy = spy(handler); ResponseInfo<ErrorResponseBody> errorResponseMock = mock(ResponseInfo.class); doReturn(errorResponseMock).when(handlerSpy).processError(state, null, exThatShouldForceCloseConnection); assertThat(state.getResponseInfo(), nullValue()); // when PipelineContinuationBehavior result = handlerSpy.doExceptionCaught(ctxMock, exThatShouldForceCloseConnection); // then verify(errorResponseMock).setForceConnectionCloseAfterResponseSent(true); verify(handlerSpy).getStateAndCreateIfNeeded(ctxMock, exThatShouldForceCloseConnection); verify(handlerSpy).processError(state, null, exThatShouldForceCloseConnection); assertThat(state.getResponseInfo(), is(errorResponseMock)); assertThat(result, is(PipelineContinuationBehavior.CONTINUE)); }
@Test public void doChannelRead_should_call_processUnhandledError_and_set_response_on_state_and_return_CONTINUE_if_request_has_not_been_handled() throws Exception { // given ExceptionHandlingHandler handlerSpy = spy(handler); ResponseInfo<ErrorResponseBody> errorResponseMock = mock(ResponseInfo.class); Object msg = new Object(); doReturn(errorResponseMock).when(handlerSpy).processUnhandledError(eq(state), eq(msg), any(Throwable.class)); assertThat(state.isRequestHandled(), is(false)); // when PipelineContinuationBehavior result = handlerSpy.doChannelRead(ctxMock, msg); // then verify(handlerSpy).processUnhandledError(eq(state), eq(msg), any(Throwable.class)); assertThat(state.getResponseInfo(), is(errorResponseMock)); Assertions.assertThat(state.getErrorThatTriggeredThisResponse()) .isInstanceOf(InvalidRipostePipelineException.class); verify(handlerSpy).addErrorAnnotationToOverallRequestSpan( state, errorResponseMock, state.getErrorThatTriggeredThisResponse() ); assertThat(result, is(PipelineContinuationBehavior.CONTINUE)); }
@Test public void processError_returns_value_of_processUnhandledError_if_riposteErrorHandler_explodes() throws UnexpectedMajorErrorHandlingError, JsonProcessingException { // given HttpProcessingState stateMock = mock(HttpProcessingState.class); Object msg = new Object(); Throwable cause = new Exception(); ExceptionHandlingHandler handlerSpy = spy(handler); ResponseInfo<ErrorResponseBody> responseInfoMockFromCatchallMethod = mock(ResponseInfo.class); doThrow(new RuntimeException()).when(riposteErrorHandlerMock).maybeHandleError(any(), any()); doReturn(responseInfoMockFromCatchallMethod).when(handlerSpy).processUnhandledError(stateMock, msg, cause); // when ResponseInfo<ErrorResponseBody> response = handlerSpy.processError(stateMock, msg, cause); // then verify(riposteErrorHandlerMock).maybeHandleError(any(), any()); assertThat(response, is(responseInfoMockFromCatchallMethod)); }
@Test public void getRequestInfo_uses_requestInfo_from_state_if_available() { // given RequestInfo<?> requestInfoMock = mock(RequestInfo.class); state.setRequestInfo(requestInfoMock); // when RequestInfo<?> result = handler.getRequestInfo(state, null); // then assertThat(result, is(requestInfoMock)); }
@Test public void doChannelRead_should_return_CONTINUE_and_do_nothing_else_if_request_has_been_handled() throws Exception { // given ExceptionHandlingHandler handlerSpy = spy(handler); ResponseInfo<?> responseInfoMock = mock(ResponseInfo.class); Object msg = new Object(); state.setResponseInfo(responseInfoMock, null); // when PipelineContinuationBehavior result = handlerSpy.doChannelRead(ctxMock, msg); // then verify(handlerSpy, times(0)).processUnhandledError(any(HttpProcessingState.class), any(Object.class), any(Throwable.class)); assertThat(state.getResponseInfo(), is(responseInfoMock)); assertThat(result, is(PipelineContinuationBehavior.CONTINUE)); }
@DataProvider(value = { "true", "false" }) @Test public void doExceptionCaught_should_do_nothing_and_return_DO_NOT_FIRE_CONTINUE_EVENT_if_response_sending_already_started( boolean causeIsNullPointerException ) { // given ExceptionHandlingHandler handlerSpy = spy(handler); Throwable cause = (causeIsNullPointerException) ? new NullPointerException("intentional NPE") : new Exception("intentional test exception"); ResponseInfo<?> responseInfoMock = mock(ResponseInfo.class); doReturn(true).when(responseInfoMock).isResponseSendingStarted(); state.setResponseInfo(responseInfoMock, cause); // when PipelineContinuationBehavior result = handlerSpy.doExceptionCaught(ctxMock, cause); // then verify(handlerSpy, never()).processError(any(HttpProcessingState.class), any(Object.class), any(Throwable.class)); Assertions.assertThat(result).isEqualTo(DO_NOT_FIRE_CONTINUE_EVENT); }
@Test public void getStateAndCreateIfNeeded_uses_state_from_ctx_if_available() { // expect assertThat(handler.getStateAndCreateIfNeeded(ctxMock, null), is(state)); }
@Test public void addErrorAnnotationToOverallRequestSpan_does_not_propagate_exceptions() { // given HttpProcessingState stateSpy = spy(state); doThrow(new RuntimeException("intentional exception")).when(stateSpy).getOverallRequestSpan(); // when Throwable ex = catchThrowable( () -> handler.addErrorAnnotationToOverallRequestSpan( stateSpy, mock(ResponseInfo.class), mock(Throwable.class) ) ); // then Assertions.assertThat(ex).isNull(); } }
@Test public void setupResponseInfoBasedOnErrorResponseInfo_sets_response_content_and_httpStatusCode_and_adds_extra_headers() { // given ResponseInfo<ErrorResponseBody> responseInfo = new FullResponseInfo<>(); ErrorResponseBody errorResponseBodyMock = mock(ErrorResponseBody.class); int httpStatusCode = 42; Map<String, List<String>> extraHeaders = new HashMap<>(); extraHeaders.put("key1", Arrays.asList("foo", "bar")); extraHeaders.put("key2", Arrays.asList("baz")); ErrorResponseInfo errorInfoMock = mock(ErrorResponseInfo.class); doReturn(errorResponseBodyMock).when(errorInfoMock).getErrorResponseBody(); doReturn(httpStatusCode).when(errorInfoMock).getErrorHttpStatusCode(); doReturn(extraHeaders).when(errorInfoMock).getExtraHeadersToAddToResponse(); // when handler.setupResponseInfoBasedOnErrorResponseInfo(responseInfo, errorInfoMock); // then assertThat(responseInfo.getContentForFullResponse(), is(errorResponseBodyMock)); assertThat(responseInfo.getHttpStatusCode(), is(httpStatusCode)); int numIndividualValuesInHeaderMap = extraHeaders.entrySet().stream().map(entry -> entry.getValue()).mapToInt(list -> list.size()).sum(); assertThat(responseInfo.getHeaders().entries().size(), is(numIndividualValuesInHeaderMap)); extraHeaders.entrySet().stream().forEach(expectedEntry -> assertThat(responseInfo.getHeaders().getAll(expectedEntry.getKey()), is(expectedEntry.getValue()))); }
Throwable cause = new Exception("intentional test exception"); ResponseInfo<ErrorResponseBody> errorResponseMock = mock(ResponseInfo.class); doReturn(errorResponseMock).when(handlerSpy).processError(state, null, cause); assertThat(state.getResponseInfo(), nullValue()); PipelineContinuationBehavior result = handlerSpy.doExceptionCaught(ctxMock, cause); verify(handlerSpy).getStateAndCreateIfNeeded(ctxMock, cause); verify(handlerSpy).processError(state, null, cause); assertThat(state.getResponseInfo(), is(errorResponseMock)); Assertions.assertThat(state.getErrorThatTriggeredThisResponse()).isSameAs(cause); verify(handlerSpy).addErrorAnnotationToOverallRequestSpan(state, errorResponseMock, cause);
@Override public PipelineContinuationBehavior doChannelRead(ChannelHandlerContext ctx, Object msg) { // 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()) { runnableWithTracingAndMdc(() -> { 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<ErrorResponseBody> responseInfo = processUnhandledError(state, msg, ex); state.setResponseInfo(responseInfo, ex); addErrorAnnotationToOverallRequestSpan(state, responseInfo, ex); }, ctx).run(); } return PipelineContinuationBehavior.CONTINUE; }
RequestInfo<?> requestInfo = exceptionHandlingHandler.getRequestInfo(state, msg); exceptionHandlingHandler.processUnhandledError(state, msg, exceptionToUse); responseSender.sendErrorResponse(ctx, requestInfo, responseInfo);