/** * Helper wrapper around {@link #handleException(Throwable, RequestInfoForLogging)} that takes in servlet request * and servlet response. The request will be wrapped in a {@link RequestInfoForLoggingServletApiAdapter} so that it * can be passed along to the method that does the work. If there are any headers in the returned {@link * ErrorResponseInfo#headersToAddToResponse} then they will be automatically added to the given servlet response, * and {@link javax.servlet.http.HttpServletResponse#setStatus(int)} will be automatically set with {@link * ErrorResponseInfo#httpStatusCode} as well. */ public ErrorResponseInfo<T> handleException(Throwable ex, HttpServletRequest servletRequest, HttpServletResponse servletResponse) { ErrorResponseInfo<T> errorResponseInfo = handleException( ex, new RequestInfoForLoggingServletApiAdapter(servletRequest) ); processServletResponse(errorResponseInfo, servletResponse); return errorResponseInfo; }
/** * Helper wrapper around {@link #maybeHandleException(Throwable, RequestInfoForLogging)} that takes in servlet * request and servlet response. The request will be wrapped in a {@link RequestInfoForLoggingServletApiAdapter} so * that it can be passed along to the method that does the work. If there are any headers in the returned {@link * ErrorResponseInfo#headersToAddToResponse} then they will be automatically added to the given servlet response, * and {@link javax.servlet.http.HttpServletResponse#setStatus(int)} will be automatically set with {@link * ErrorResponseInfo#httpStatusCode} as well. */ public ErrorResponseInfo<T> maybeHandleException( Throwable ex, HttpServletRequest servletRequest, HttpServletResponse servletResponse ) throws UnexpectedMajorExceptionHandlingError { ErrorResponseInfo<T> errorResponseInfo = maybeHandleException( ex, new RequestInfoForLoggingServletApiAdapter(servletRequest) ); if (errorResponseInfo != null) processServletResponse(errorResponseInfo, servletResponse); return errorResponseInfo; }
/** * @return An {@link ApiExceptionHandlerListenerResult} indicating whether we should handle the given exception. * If {@link ApiExceptionHandlerListenerResult#shouldHandleResponse} is true then * {@link ApiExceptionHandlerListenerResult#errors} and * {@link ApiExceptionHandlerListenerResult#extraDetailsForLogging} must be filled in appropriately and * ready for passing in to * {@link #doHandleApiException(SortedApiErrorSet, List, List, Throwable, RequestInfoForLogging)}. If it is * false then the given exception will be ignored by this class (and should therefore ultimately be handled * by this project's implementation of {@link UnhandledExceptionHandlerBase}). */ protected ApiExceptionHandlerListenerResult shouldHandleApiException(Throwable ex) { // The original exception might be a "wrapper" exception. If so, unwrap it so we can send the core exception // through our list of listeners. Throwable coreEx = unwrapAndFindCoreException(ex); // Run through each listener looking for one that wants to handle the core exception. for (ApiExceptionHandlerListener listener : apiExceptionHandlerListenerList) { ApiExceptionHandlerListenerResult result = listener.shouldHandleException(coreEx); if (result.shouldHandleResponse) return result; } // We didn't have any handler that wanted to deal with this exception, so return an "ignore it" response. return ApiExceptionHandlerListenerResult.ignoreResponse(); }
extraDetailsForLogging.add(Pair.of("unhandled_error", "true")); if (logRequestBodyOnUnhandledExceptions(ex, request)) { String body; try { body = request.getBody(); } catch (RequestInfoForLogging.GetBodyException e) { logger.warn("Failed to retrieve request_body while handling exception ex=" + ex, e); errorUid = utils.buildErrorMessageForLogs( baseLogMessageBuilder, request, errorsToUse, httpStatusCodeToUse, ex, extraDetailsForLogging ); T frameworkRepresentation = prepareFrameworkRepresentation( errorContractDTO, httpStatusCodeToUse, errorsToUse, ex, request ); Map<String, List<String>> extraHeadersForResponse = extraHeadersForResponse( frameworkRepresentation, errorContractDTO, httpStatusCodeToUse, errorsToUse, ex, request ); return new ErrorResponseInfo<>(httpStatusCodeToUse, frameworkRepresentation, finalHeadersForResponse); return generateLastDitchFallbackErrorResponseInfo(ex, request, errorUid, finalHeadersForResponse);
@Test public void handleErrorFromNettyInterfaceWrapsRequestInfoWithAdapterBeforeContinuing() { com.nike.backstopper.handler.ErrorResponseInfo<ErrorResponseBody> backstopperResponse = new ErrorResponseInfo<>(42, mock(ErrorResponseBody.class), Collections.emptyMap()); doReturn(backstopperResponse).when(adapterSpy).handleException(any(Throwable.class), any(RequestInfoForLogging.class)); RequestInfo requestInfoMock = mock(RequestInfo.class); adapterSpy.handleError(new Exception(), requestInfoMock); ArgumentCaptor<RequestInfoForLogging> requestInfoForLoggingArgumentCaptor = ArgumentCaptor.forClass(RequestInfoForLogging.class); verify(adapterSpy).handleException(any(Throwable.class), requestInfoForLoggingArgumentCaptor.capture()); RequestInfoForLogging passedArg = requestInfoForLoggingArgumentCaptor.getValue(); assertThat(passedArg, instanceOf(RequestInfoForLoggingRiposteAdapter.class)); RequestInfo embeddedRequestInfoInWrapper = (RequestInfo) Whitebox.getInternalState(passedArg, "request"); assertThat(embeddedRequestInfoInWrapper, sameInstance(requestInfoMock)); }
@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)); }
String traceId = extractDistributedTraceId(request); String requestUri = request.getRequestUri(); String requestMethod = request.getRequestHttpMethod(); String queryString = request.getQueryString(); String headersString = parseRequestHeadersToString(request); String contributingErrorsString = concatenateErrorCollection(contributingErrors);
@Override protected com.nike.backstopper.handler.ErrorResponseInfo<ErrorResponseBody> generateLastDitchFallbackErrorResponseInfo( Throwable ex, RequestInfoForLogging request, String errorUid, Map<String, List<String>> headersForResponseWithErrorUid ) { return new com.nike.backstopper.handler.ErrorResponseInfo<>( genericServiceErrorHttpStatusCode, new ErrorResponseBodyImpl(errorUid, singletonGenericServiceError), headersForResponseWithErrorUid ); }
@Test(expected = UnexpectedMajorErrorHandlingError.class) public void maybeHandleErrorExplosionThrowsUnexpectedMajorErrorHandlingError() throws UnexpectedMajorExceptionHandlingError, UnexpectedMajorErrorHandlingError { UnexpectedMajorExceptionHandlingError innerExplosion = new UnexpectedMajorExceptionHandlingError("intentional kaboom", new Exception()); doThrow(innerExplosion).when(adapterSpy).maybeHandleException(any(Throwable.class), any(RequestInfoForLogging.class)); RequestInfo requestInfoMock = mock(RequestInfo.class); adapterSpy.maybeHandleError(new Exception(), requestInfoMock); } }
ApiExceptionHandlerListenerResult result = shouldHandleApiException(ex); return doHandleApiException(result.errors, result.extraDetailsForLogging, result.extraResponseHeaders, ex, request); throw new UnexpectedMajorExceptionHandlingError( "Unexpected major error in " + this.getClass().getName() + ". We had an inner exception while trying " + "to handle the original controller exception. This needs to be fixed ASAP. "
protected final Pair<String, String> causeDetailsForLogs(Throwable orig) { Throwable cause = orig.getCause(); String causeDetails = (cause == null) ? "NO_CAUSE" : cause.toString(); return Pair.of("exception_cause_details", ApiExceptionHandlerUtils.DEFAULT_IMPL.quotesToApostrophes(causeDetails)); }
/** * @return All the headers in the given request as a comma-separated list of name=value in string form. * Multi-value headers will come back in name=[value1,value2] form. * NOTE: This method never throws an exception. If it catches one it will return blank string "" instead. */ public String parseRequestHeadersToString(RequestInfoForLogging request) { try { Map<String, List<String>> headers = request.getHeadersMap(); if (headers == null || headers.isEmpty()) return ""; Set<String> headerNames = headers.keySet(); StringBuilder sb = new StringBuilder(); boolean first = true; for (String headerName : headerNames) { if (!first) sb.append(","); sb.append(parseSpecificHeaderToString(request, headerName)); first = false; } return sb.toString(); } catch(Exception ex) { return ""; } }
@SafeVarargs protected final List<Pair<String, String>> withBaseExceptionMessage( Throwable ex, Pair<String, String>... extraLogMessages ) { List<Pair<String, String>> logPairs = new ArrayList<>(); ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging(ex, logPairs); if (extraLogMessages != null) { logPairs.addAll(Arrays.asList(extraLogMessages)); } return logPairs; }
/** * "Unwraps" the given exception by digging through the {@link Throwable#getCause()} chain until a non-wrapper * exception type is found. Uses {@link #getWrapperExceptionClassNames()} as the set of exception classes that are * considered wrappers. * * @param error The exception that may (or may not) need to be "unwrapped". * @return The root/core cause exception that is not a wrapper exception - the passed-in exception will be returned * as-is if it is not a wrapper exception or if it has no cause. */ protected Throwable unwrapAndFindCoreException(Throwable error) { if (error == null || error.getCause() == null || error.getCause() == error) return error; // At this point there must be a non-null cause, and it is not a reference to itself. See if it's a wrapper. if (getWrapperExceptionClassNames().contains(error.getClass().getName())) { // This is a wrapper. Extract the cause. error = error.getCause(); // Recursively unwrap until we get something that is not unwrappable error = unwrapAndFindCoreException(error); } return error; }
/** * @return The distributed trace ID if available in the request or the SLF4J {@link MDC}, or null if it cannot be * found. Will also return null if the distributed trace ID exists but its trimmed length is 0 * (i.e. the distributed trace ID must be non-empty and contain something besides whitespace for it to be * used). If you are using a distributed tracing system that uses different keys or where the trace ID is * otherwise unobtainable using the rules defined here, then you can override this method and provide * whatever rules you want. */ public String extractDistributedTraceId(RequestInfoForLogging request) { String traceIdToUse = null; if (distributedTraceIdHeaderKey != null) { String dtraceIdFromHeader = request.getHeader(distributedTraceIdHeaderKey); Object dtraceIdFromAttribute = request.getAttribute(distributedTraceIdHeaderKey); if (StringUtils.isNotBlank(dtraceIdFromHeader)) traceIdToUse = dtraceIdFromHeader.trim(); else if (dtraceIdFromAttribute != null && StringUtils.isNotBlank(dtraceIdFromAttribute.toString())) traceIdToUse = dtraceIdFromAttribute.toString().trim(); } if (traceIdToUse == null) { // As a last resort try to get it from the MDC since some distributed systems (e.g. Wingtips) put the // trace ID there. String fromMdc = MDC.get(TRACE_ID_MDC_KEY); if (fromMdc != null) traceIdToUse = fromMdc.trim(); } return traceIdToUse; }
return headerName + "=[MASKED]"; } else { List<String> headerValues = request.getHeaders(headerName); if (headerValues == null || headerValues.isEmpty()) return "";
@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)); }
@Override protected ErrorResponseInfo<Response.ResponseBuilder> generateLastDitchFallbackErrorResponseInfo(Throwable ex, RequestInfoForLogging request, String errorUid, Map<String, List<String>> headersForResponseWithErrorUid) { DefaultErrorContractDTO errorContract = new DefaultErrorContractDTO(errorUid, singletonGenericServiceError); return new ErrorResponseInfo<>( genericServiceErrorHttpStatusCode, Response.status(genericServiceErrorHttpStatusCode).entity( JsonUtilWithDefaultErrorContractDTOSupport.writeValueAsString(errorContract) ), headersForResponseWithErrorUid ); } }
/** * Adds the given exception's {@link Exception#getMessage()} to the given extraDetailsForLogging with the key of * "exception_message" and with the exception's message pruned of quotes via {@link #quotesToApostrophes(String)}. */ public void addBaseExceptionMessageToExtraDetailsForLogging(Throwable ex, List<Pair<String, String>> extraDetailsForLogging) { extraDetailsForLogging.add(Pair.of("exception_message", quotesToApostrophes(ex.getMessage()))); }
@Test public void constructorWithErrorResponseInfoArgsSetsValues() { ErrorResponseBody bodyMock = mock(ErrorResponseBody.class); int httpStatusCode = 42; Map<String, List<String>> headers = MapBuilder.<String, List<String>>builder().put("someHeader", Arrays.asList("hval1", "hval2")).build(); ErrorResponseInfo<ErrorResponseBody> backstopperErrorResponseInfo = new ErrorResponseInfo<>(httpStatusCode, bodyMock, headers); ErrorResponseInfoImpl adapter = new ErrorResponseInfoImpl(backstopperErrorResponseInfo); assertThat(adapter.errorResponseBody, sameInstance(bodyMock)); assertThat(adapter.httpStatusCode, is(httpStatusCode)); assertThat(adapter.headersToAddToResponse, is(headers)); }