public static ErrorResponseBodySerializer asErrorResponseBodySerializer(ObjectMapper objectMapper) { return errorResponseBody -> { try { Object bodyToSerialize = (errorResponseBody == null) ? null : errorResponseBody.bodyToSerialize(); if (bodyToSerialize == null) { // errorResponseBody itself is null, or errorResponseBody.bodyToSerialize() is null. Either case // indicates empty response body payload, so we should return null. return null; } if(bodyToSerialize instanceof CharSequence) { return bodyToSerialize.toString(); } else { return objectMapper.writeValueAsString(bodyToSerialize); } } catch (JsonProcessingException e) { throw new RuntimeException("An error occurred while serializing an ErrorResponseBody to a string", e); } }; } }
protected void setupResponseInfoBasedOnErrorResponseInfo(ResponseInfo<ErrorResponseBody> responseInfo, ErrorResponseInfo errorInfo) { responseInfo.setContentForFullResponse(errorInfo.getErrorResponseBody()); responseInfo.setHttpStatusCode(errorInfo.getErrorHttpStatusCode()); Map<String, List<String>> extraHeaders = errorInfo.getExtraHeadersToAddToResponse(); if (extraHeaders != null) { for (Map.Entry<String, List<String>> headerEntry : extraHeaders.entrySet()) { responseInfo.getHeaders().add(headerEntry.getKey(), headerEntry.getValue()); } } }
/** * Sets an error_uid header based on the given error response's {@link ErrorResponseBody#errorId()} and replaces the * {@link ErrorResponseBody} found in the {@link ResponseInfo#getContentForFullResponse()} with the String result of * calling {@link ErrorResponseBodySerializer#serializeErrorResponseBodyToString(ErrorResponseBody)} on {@link * #errorResponseBodySerializer}. The modified {@link ResponseInfo} is then sent to {@link * #sendFullResponse(io.netty.channel.ChannelHandlerContext, RequestInfo, ResponseInfo, ObjectMapper)} for passing * back to the client. * <p/> * NOTE: This assumes a full (not chunked) response, and uses {@link ResponseInfo#getContentForFullResponse()} to * retrieve the {@link ErrorResponseBody} object. Therefore this method will throw an {@link * IllegalArgumentException} if you pass in a response object that returns true for {@link * ResponseInfo#isChunkedResponse()}. */ public void sendErrorResponse(ChannelHandlerContext ctx, RequestInfo requestInfo, ResponseInfo<ErrorResponseBody> responseInfo) throws JsonProcessingException { if (responseInfo.isChunkedResponse()) { throw new IllegalArgumentException("The responseInfo argument is marked as being a chunked response, but " + "sendErrorResponse(...) only works with full responses"); } responseInfo.getHeaders().set("error_uid", responseInfo.getContentForFullResponse().errorId()); @SuppressWarnings("UnnecessaryLocalVariable") ErrorResponseBody bodyToSerialize = responseInfo.getContentForFullResponse(); if (bodyToSerialize != null) { String errorBodyAsString = errorResponseBodySerializer.serializeErrorResponseBodyToString(bodyToSerialize); //noinspection unchecked ((ResponseInfo) responseInfo).setContentForFullResponse(errorBodyAsString); } sendFullResponse(ctx, requestInfo, responseInfo, defaultResponseContentSerializer); }
@Test public void asErrorResponseBodySerializer_returns_serializer_that_serializes_as_expected( ) throws JsonProcessingException { // given ObjectMapper objectMapperMock = mock(ObjectMapper.class); ErrorResponseBodySerializer serializer = asErrorResponseBodySerializer(objectMapperMock); String objectMapperResult = UUID.randomUUID().toString(); doReturn(objectMapperResult).when(objectMapperMock).writeValueAsString(anyObject()); Object objectToSerialize = new Object(); ErrorResponseBody errorResponseBodyMock = mock(ErrorResponseBody.class); doReturn(objectToSerialize).when(errorResponseBodyMock).bodyToSerialize(); // when String result = serializer.serializeErrorResponseBodyToString(errorResponseBodyMock); // then assertThat(result).isEqualTo(objectMapperResult); verify(objectMapperMock).writeValueAsString(objectToSerialize); }
/** * 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; }
@DataProvider(value = { "NULL_INSTANCE", "NULL_BODY_TO_SERIALIZE" }) @Test public void asErrorResponseBodySerializer_returns_serializer_that_returns_null_when_it_is_supposed_to( ErrorResponseBodyScenario scenario ) { // given ErrorResponseBody errorResponseBody = scenario.generateErrorResponseBody(); ObjectMapper objectMapperMock = mock(ObjectMapper.class); ErrorResponseBodySerializer serializer = asErrorResponseBodySerializer(objectMapperMock); // when String result = serializer.serializeErrorResponseBodyToString(errorResponseBody); // then assertThat(result).isNull(); verifyZeroInteractions(objectMapperMock); }
protected void setupResponseInfoBasedOnErrorResponseInfo(ResponseInfo<ErrorResponseBody> responseInfo, ErrorResponseInfo errorInfo) { responseInfo.setContentForFullResponse(errorInfo.getErrorResponseBody()); responseInfo.setHttpStatusCode(errorInfo.getErrorHttpStatusCode()); Map<String, List<String>> extraHeaders = errorInfo.getExtraHeadersToAddToResponse(); if (extraHeaders != null) { for (Map.Entry<String, List<String>> headerEntry : extraHeaders.entrySet()) { responseInfo.getHeaders().add(headerEntry.getKey(), headerEntry.getValue()); } } }
@Test public void asErrorResponseBodySerializer_returns_serializer_that_propagates_JsonProcessingException_as_RuntimeException( ) throws JsonProcessingException { // given ObjectMapper objectMapperMock = mock(ObjectMapper.class); ErrorResponseBodySerializer serializer = asErrorResponseBodySerializer(objectMapperMock); ErrorResponseBody errorResponseBodyMock = mock(ErrorResponseBody.class); doReturn(new Object()).when(errorResponseBodyMock).bodyToSerialize(); JsonProcessingException jsonProcessingExceptionMock = mock(JsonProcessingException.class); doThrow(jsonProcessingExceptionMock).when(objectMapperMock).writeValueAsString(anyObject()); // when Throwable ex = catchThrowable(() -> serializer.serializeErrorResponseBodyToString(errorResponseBodyMock)); // then assertThat(ex) .isNotNull() .isExactlyInstanceOf(RuntimeException.class) .hasCause(jsonProcessingExceptionMock); }
/** * Sets an error_uid header based on the given error response's {@link ErrorResponseBody#errorId()} and replaces the * {@link ErrorResponseBody} found in the {@link ResponseInfo#getContentForFullResponse()} with the String result of * calling {@link ErrorResponseBodySerializer#serializeErrorResponseBodyToString(ErrorResponseBody)} on {@link * #errorResponseBodySerializer}. The modified {@link ResponseInfo} is then sent to {@link * #sendFullResponse(io.netty.channel.ChannelHandlerContext, RequestInfo, ResponseInfo, ObjectMapper)} for passing * back to the client. * <p/> * NOTE: This assumes a full (not chunked) response, and uses {@link ResponseInfo#getContentForFullResponse()} to * retrieve the {@link ErrorResponseBody} object. Therefore this method will throw an {@link * IllegalArgumentException} if you pass in a response object that returns true for {@link * ResponseInfo#isChunkedResponse()}. */ public void sendErrorResponse(ChannelHandlerContext ctx, RequestInfo requestInfo, ResponseInfo<ErrorResponseBody> responseInfo) throws JsonProcessingException { if (responseInfo.isChunkedResponse()) { throw new IllegalArgumentException("The responseInfo argument is marked as being a chunked response, but " + "sendErrorResponse(...) only works with full responses"); } responseInfo.getHeaders().set("error_uid", responseInfo.getContentForFullResponse().errorId()); @SuppressWarnings("UnnecessaryLocalVariable") ErrorResponseBody bodyToSerialize = responseInfo.getContentForFullResponse(); if (bodyToSerialize != null) { String errorBodyAsString = errorResponseBodySerializer.serializeErrorResponseBodyToString(bodyToSerialize); //noinspection unchecked ((ResponseInfo) responseInfo).setContentForFullResponse(errorBodyAsString); } sendFullResponse(ctx, requestInfo, responseInfo, defaultResponseContentSerializer); }
public static ErrorResponseBodySerializer asErrorResponseBodySerializer(ObjectMapper objectMapper) { return errorResponseBody -> { try { Object bodyToSerialize = (errorResponseBody == null) ? null : errorResponseBody.bodyToSerialize(); if (bodyToSerialize == null) { // errorResponseBody itself is null, or errorResponseBody.bodyToSerialize() is null. Either case // indicates empty response body payload, so we should return null. return null; } if(bodyToSerialize instanceof CharSequence) { return bodyToSerialize.toString(); } else { return objectMapper.writeValueAsString(bodyToSerialize); } } catch (JsonProcessingException e) { throw new RuntimeException("An error occurred while serializing an ErrorResponseBody to a string", e); } }; } }
/** * 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; }
@Test public void maybeHandleErrorFromNettyInterfaceReturnsWrapperAroundBackstopperMaybeHandleExceptionReturnValue() throws UnexpectedMajorExceptionHandlingError, UnexpectedMajorErrorHandlingError { ErrorResponseBody errorResponseBodyMock = mock(ErrorResponseBody.class); Map<String, List<String>> headersMap = MapBuilder.<String, List<String>>builder().put("headerName", Arrays.asList("hval1", "hval2")).build(); com.nike.backstopper.handler.ErrorResponseInfo<ErrorResponseBody> backstopperResponse = new ErrorResponseInfo<>(42, errorResponseBodyMock, headersMap); doReturn(backstopperResponse).when(adapterSpy).maybeHandleException(any(Throwable.class), any(RequestInfoForLogging.class)); com.nike.riposte.server.error.handler.ErrorResponseInfo riposteErrorResponseInfo = adapterSpy.maybeHandleError(new Exception(), mock(RequestInfo.class)); assertThat(riposteErrorResponseInfo, instanceOf(ErrorResponseInfoImpl.class)); assertThat(riposteErrorResponseInfo.getErrorHttpStatusCode(), is(backstopperResponse.httpStatusCode)); assertThat(riposteErrorResponseInfo.getErrorResponseBody(), is(errorResponseBodyMock)); assertThat(riposteErrorResponseInfo.getExtraHeadersToAddToResponse(), is(headersMap)); }
@Test public void bodyToSerialize_returns_same_instance_by_default() { ErrorResponseBody instance = () -> "someErrorId"; assertThat(instance.bodyToSerialize()).isSameAs(instance); } }
@Test public void processError_returns_value_of_processUnhandledError_if_riposteErrorHandler_returns_null() 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); doReturn(null).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 processUnhandledError_uses_getRequestInfo_and_calls_riposteUnhandledErrorHandler_and_returns_value_of_setupResponseInfoBasedOnErrorResponseInfo() throws JsonProcessingException, UnexpectedMajorErrorHandlingError { // 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); doReturn(requestInfoMock).when(handlerSpy).getRequestInfo(stateMock, msg); doReturn(errorResponseInfoMock).when(riposteUnhandledErrorHandlerMock).handleError(cause, requestInfoMock); // when ResponseInfo<ErrorResponseBody> response = handlerSpy.processUnhandledError(stateMock, msg, cause); // then verify(handlerSpy).getRequestInfo(stateMock, msg); verify(riposteUnhandledErrorHandlerMock).handleError(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)); }
@Test public void handleErrorFromNettyInterfaceReturnsWrapperAroundBackstopperHandleExceptionReturnValue() { ErrorResponseBody errorResponseBodyMock = mock(ErrorResponseBody.class); Map<String, List<String>> headersMap = MapBuilder.<String, List<String>>builder().put("headerName", Arrays.asList("hval1", "hval2")).build(); com.nike.backstopper.handler.ErrorResponseInfo<ErrorResponseBody> backstopperResponse = new ErrorResponseInfo<>(42, errorResponseBodyMock, headersMap); doReturn(backstopperResponse).when(adapterSpy).handleException(any(Throwable.class), any(RequestInfoForLogging.class)); com.nike.riposte.server.error.handler.ErrorResponseInfo riposteErrorResponseInfo = adapterSpy.handleError(new Exception(), mock(RequestInfo.class)); assertThat(riposteErrorResponseInfo, instanceOf(ErrorResponseInfoImpl.class)); assertThat(riposteErrorResponseInfo.getErrorHttpStatusCode(), is(backstopperResponse.httpStatusCode)); assertThat(riposteErrorResponseInfo.getErrorResponseBody(), is(errorResponseBodyMock)); assertThat(riposteErrorResponseInfo.getExtraHeadersToAddToResponse(), is(headersMap)); }
@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 setupResponseInfoBasedOnErrorResponseInfo_sets_response_content_and_httpStatusCode_and_ignores_extra_headers_if_extra_headers_is_null() { // given ResponseInfo<ErrorResponseBody> responseInfo = new FullResponseInfo<>(); ErrorResponseBody errorResponseBodyMock = mock(ErrorResponseBody.class); int httpStatusCode = 42; ErrorResponseInfo errorInfoMock = mock(ErrorResponseInfo.class); doReturn(errorResponseBodyMock).when(errorInfoMock).getErrorResponseBody(); doReturn(httpStatusCode).when(errorInfoMock).getErrorHttpStatusCode(); doReturn(null).when(errorInfoMock).getExtraHeadersToAddToResponse(); // when handler.setupResponseInfoBasedOnErrorResponseInfo(responseInfo, errorInfoMock); // then assertThat(responseInfo.getContentForFullResponse(), is(errorResponseBodyMock)); assertThat(responseInfo.getHttpStatusCode(), is(httpStatusCode)); assertThat(responseInfo.getHeaders().entries().size(), is(0)); }