001package ca.uhn.fhir.rest.server.interceptor; 002 003/* 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022import ca.uhn.fhir.i18n.Msg; 023import static org.apache.commons.lang3.StringUtils.isNotBlank; 024 025import java.io.Closeable; 026import java.io.IOException; 027import java.util.Collections; 028import java.util.List; 029import java.util.Map; 030import java.util.Map.Entry; 031 032import javax.servlet.ServletException; 033import javax.servlet.http.HttpServletRequest; 034import javax.servlet.http.HttpServletResponse; 035 036import ca.uhn.fhir.interceptor.api.Hook; 037import ca.uhn.fhir.interceptor.api.Interceptor; 038import ca.uhn.fhir.interceptor.api.Pointcut; 039import ca.uhn.fhir.parser.DataFormatException; 040import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 041import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding; 042import org.apache.commons.lang3.exception.ExceptionUtils; 043import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 044 045import ca.uhn.fhir.context.FhirContext; 046import ca.uhn.fhir.rest.api.Constants; 047import ca.uhn.fhir.rest.api.SummaryEnum; 048import ca.uhn.fhir.rest.api.server.IRestfulResponse; 049import ca.uhn.fhir.rest.api.server.RequestDetails; 050import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 051import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 052import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; 053import ca.uhn.fhir.util.OperationOutcomeUtil; 054 055@Interceptor 056public class ExceptionHandlingInterceptor { 057 058 public static final String PROCESSING = Constants.OO_INFOSTATUS_PROCESSING; 059 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionHandlingInterceptor.class); 060 private Class<?>[] myReturnStackTracesForExceptionTypes; 061 062 @Hook(Pointcut.SERVER_HANDLE_EXCEPTION) 063 public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException { 064 Closeable writer = (Closeable) handleException(theRequestDetails, theException); 065 writer.close(); 066 return false; 067 } 068 069 public Object handleException(RequestDetails theRequestDetails, BaseServerResponseException theException) 070 throws ServletException, IOException { 071 IRestfulResponse response = theRequestDetails.getResponse(); 072 073 FhirContext ctx = theRequestDetails.getServer().getFhirContext(); 074 075 IBaseOperationOutcome oo = theException.getOperationOutcome(); 076 if (oo == null) { 077 oo = createOperationOutcome(theException, ctx); 078 } 079 080 int statusCode = theException.getStatusCode(); 081 082 // Add headers associated with the specific error code 083 if (theException.hasResponseHeaders()) { 084 Map<String, List<String>> additional = theException.getResponseHeaders(); 085 for (Entry<String, List<String>> next : additional.entrySet()) { 086 if (isNotBlank(next.getKey()) && next.getValue() != null) { 087 String nextKey = next.getKey(); 088 for (String nextValue : next.getValue()) { 089 response.addHeader(nextKey, nextValue); 090 } 091 } 092 } 093 } 094 095 String statusMessage = null; 096 if (theException instanceof UnclassifiedServerFailureException) { 097 String sm = theException.getMessage(); 098 if (isNotBlank(sm) && sm.indexOf('\n') == -1) { 099 statusMessage = sm; 100 } 101 } 102 103 BaseResourceReturningMethodBinding.callOutgoingFailureOperationOutcomeHook(theRequestDetails, oo); 104 return response.streamResponseAsResource(oo, true, Collections.singleton(SummaryEnum.FALSE), statusCode, statusMessage, false, false); 105 106 } 107 108 @Hook(Pointcut.SERVER_PRE_PROCESS_OUTGOING_EXCEPTION) 109 public BaseServerResponseException preProcessOutgoingException(RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest) throws ServletException { 110 BaseServerResponseException retVal; 111 if (theException instanceof DataFormatException) { 112 // Wrapping the DataFormatException as an InvalidRequestException so that it gets sent back to the client as a 400 response. 113 retVal = new InvalidRequestException(theException); 114 } else if (!(theException instanceof BaseServerResponseException)) { 115 retVal = new InternalErrorException(theException); 116 } else { 117 retVal = (BaseServerResponseException) theException; 118 } 119 120 if (retVal.getOperationOutcome() == null) { 121 retVal.setOperationOutcome(createOperationOutcome(theException, theRequestDetails.getServer().getFhirContext())); 122 } 123 124 return retVal; 125 } 126 127 private IBaseOperationOutcome createOperationOutcome(Throwable theException, FhirContext ctx) throws ServletException { 128 IBaseOperationOutcome oo = null; 129 if (theException instanceof BaseServerResponseException) { 130 oo = ((BaseServerResponseException) theException).getOperationOutcome(); 131 } 132 133 /* 134 * Generate an OperationOutcome to return, unless the exception throw by the resource provider had one 135 */ 136 if (oo == null) { 137 try { 138 oo = OperationOutcomeUtil.newInstance(ctx); 139 140 if (theException instanceof InternalErrorException) { 141 ourLog.error("Failure during REST processing", theException); 142 populateDetails(ctx, theException, oo); 143 } else if (theException instanceof BaseServerResponseException) { 144 int statusCode = ((BaseServerResponseException) theException).getStatusCode(); 145 146 // No stack traces for non-server internal errors 147 if (statusCode < 500) { 148 ourLog.warn("Failure during REST processing: {}", theException.toString()); 149 } else { 150 ourLog.warn("Failure during REST processing", theException); 151 } 152 153 BaseServerResponseException baseServerResponseException = (BaseServerResponseException) theException; 154 populateDetails(ctx, theException, oo); 155 if (baseServerResponseException.getAdditionalMessages() != null) { 156 for (String next : baseServerResponseException.getAdditionalMessages()) { 157 OperationOutcomeUtil.addIssue(ctx, oo, "error", next, null, PROCESSING); 158 } 159 } 160 } else { 161 ourLog.error("Failure during REST processing: " + theException.toString(), theException); 162 populateDetails(ctx, theException, oo); 163 } 164 } catch (Exception e1) { 165 ourLog.error("Failed to instantiate OperationOutcome resource instance", e1); 166 throw new ServletException(Msg.code(328) + "Failed to instantiate OperationOutcome resource instance", e1); 167 } 168 } else { 169 ourLog.error("Unknown error during processing", theException); 170 } 171 return oo; 172 } 173 174 private void populateDetails(FhirContext theCtx, Throwable theException, IBaseOperationOutcome theOo) { 175 if (myReturnStackTracesForExceptionTypes != null) { 176 for (Class<?> next : myReturnStackTracesForExceptionTypes) { 177 if (next.isAssignableFrom(theException.getClass())) { 178 String detailsValue = theException.getMessage() + "\n\n" + ExceptionUtils.getStackTrace(theException); 179 OperationOutcomeUtil.addIssue(theCtx, theOo, "error", detailsValue, null, PROCESSING); 180 return; 181 } 182 } 183 } 184 185 OperationOutcomeUtil.addIssue(theCtx, theOo, "error", theException.getMessage(), null, PROCESSING); 186 } 187 188 /** 189 * If any server methods throw an exception which extends any of the given exception types, the exception stack trace will be returned to the user. This can be useful for helping to diagnose 190 * issues, but may not be desirable for production situations. 191 * 192 * @param theExceptionTypes 193 * The exception types for which to return the stack trace to the user. 194 * @return Returns an instance of this interceptor, to allow for easy method chaining. 195 */ 196 public ExceptionHandlingInterceptor setReturnStackTracesForExceptionTypes(Class<?>... theExceptionTypes) { 197 myReturnStackTracesForExceptionTypes = theExceptionTypes; 198 return this; 199 } 200 201}