001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.server.interceptor; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.Hook; 025import ca.uhn.fhir.interceptor.api.Interceptor; 026import ca.uhn.fhir.interceptor.api.Pointcut; 027import ca.uhn.fhir.parser.DataFormatException; 028import ca.uhn.fhir.rest.api.Constants; 029import ca.uhn.fhir.rest.api.SummaryEnum; 030import ca.uhn.fhir.rest.api.server.IRestfulResponse; 031import ca.uhn.fhir.rest.api.server.RequestDetails; 032import ca.uhn.fhir.rest.server.RestfulServerUtils; 033import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 034import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 035import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 036import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; 037import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding; 038import ca.uhn.fhir.rest.server.servlet.ServletRestfulResponse; 039import ca.uhn.fhir.util.OperationOutcomeUtil; 040import jakarta.servlet.ServletException; 041import jakarta.servlet.http.HttpServletRequest; 042import jakarta.servlet.http.HttpServletResponse; 043import org.apache.commons.collections4.map.HashedMap; 044import org.apache.commons.lang3.exception.ExceptionUtils; 045import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 046 047import java.io.IOException; 048import java.util.Collection; 049import java.util.Collections; 050import java.util.List; 051import java.util.Map; 052import java.util.Map.Entry; 053import java.util.Set; 054 055import static org.apache.commons.lang3.StringUtils.isNotBlank; 056import static org.apache.http.HttpHeaders.CONTENT_ENCODING; 057 058@Interceptor 059public class ExceptionHandlingInterceptor { 060 061 public static final String PROCESSING = Constants.OO_INFOSTATUS_PROCESSING; 062 private static final org.slf4j.Logger ourLog = 063 org.slf4j.LoggerFactory.getLogger(ExceptionHandlingInterceptor.class); 064 public static final Set<SummaryEnum> SUMMARY_MODE = Collections.singleton(SummaryEnum.FALSE); 065 private Class<?>[] myReturnStackTracesForExceptionTypes; 066 067 @Hook(Pointcut.SERVER_HANDLE_EXCEPTION) 068 public boolean handleException( 069 RequestDetails theRequestDetails, 070 BaseServerResponseException theException, 071 HttpServletRequest theRequest, 072 HttpServletResponse theResponse) 073 throws ServletException, IOException { 074 handleException(theRequestDetails, theException); 075 return false; 076 } 077 078 public Object handleException(RequestDetails theRequestDetails, BaseServerResponseException theException) 079 throws ServletException, IOException { 080 IRestfulResponse response = theRequestDetails.getResponse(); 081 082 FhirContext ctx = theRequestDetails.getServer().getFhirContext(); 083 084 IBaseOperationOutcome oo = theException.getOperationOutcome(); 085 if (oo == null) { 086 oo = createOperationOutcome(theException, ctx); 087 } 088 089 int statusCode = theException.getStatusCode(); 090 091 // Add headers associated with the specific error code 092 if (theException.hasResponseHeaders()) { 093 Map<String, List<String>> additional = theException.getResponseHeaders(); 094 for (Entry<String, List<String>> next : additional.entrySet()) { 095 if (isNotBlank(next.getKey()) && next.getValue() != null) { 096 String nextKey = next.getKey(); 097 for (String nextValue : next.getValue()) { 098 response.addHeader(nextKey, nextValue); 099 } 100 } 101 } 102 } 103 104 String statusMessage = null; 105 if (theException instanceof UnclassifiedServerFailureException) { 106 String sm = theException.getMessage(); 107 if (isNotBlank(sm) && sm.indexOf('\n') == -1) { 108 statusMessage = sm; 109 } 110 } 111 112 BaseResourceReturningMethodBinding.callOutgoingFailureOperationOutcomeHook(theRequestDetails, oo); 113 try { 114 resetOutputStreamIfPossible(response); 115 } catch (Throwable t) { 116 ourLog.error( 117 "HAPI-FHIR was unable to reset the output stream during exception handling. The root causes follows:", 118 t); 119 } 120 121 return RestfulServerUtils.streamResponseAsResource( 122 theRequestDetails.getServer(), 123 oo, 124 SUMMARY_MODE, 125 statusCode, 126 false, 127 false, 128 theRequestDetails, 129 null, 130 null); 131 } 132 133 /** 134 * In some edge cases, the output stream is opened already by the point at which an exception is thrown. 135 * This is a failsafe to that the output stream is writeable in that case. This operation retains status code and headers, but clears the buffer. 136 * Also, it strips the content-encoding header if present, as the method outcome will negotiate its own. 137 */ 138 private void resetOutputStreamIfPossible(IRestfulResponse response) { 139 if (response.getClass().isAssignableFrom(ServletRestfulResponse.class)) { 140 ServletRestfulResponse servletRestfulResponse = (ServletRestfulResponse) response; 141 HttpServletResponse servletResponse = 142 servletRestfulResponse.getRequestDetails().getServletResponse(); 143 Collection<String> headerNames = servletResponse.getHeaderNames(); 144 Map<String, Collection<String>> oldHeaders = new HashedMap<>(); 145 for (String headerName : headerNames) { 146 oldHeaders.put(headerName, servletResponse.getHeaders(headerName)); 147 } 148 149 servletResponse.reset(); 150 oldHeaders.entrySet().stream() 151 .filter(entry -> !entry.getKey().equals(CONTENT_ENCODING)) 152 .forEach(entry -> { 153 entry.getValue().stream().forEach(value -> { 154 servletResponse.addHeader(entry.getKey(), value); 155 }); 156 }); 157 } 158 } 159 160 @Hook(Pointcut.SERVER_PRE_PROCESS_OUTGOING_EXCEPTION) 161 public BaseServerResponseException preProcessOutgoingException( 162 RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest) 163 throws ServletException { 164 BaseServerResponseException retVal; 165 if (theException instanceof DataFormatException) { 166 // Wrapping the DataFormatException as an InvalidRequestException so that it gets sent back to the client as 167 // a 400 response. 168 retVal = new InvalidRequestException(theException); 169 } else if (!(theException instanceof BaseServerResponseException)) { 170 retVal = new InternalErrorException(theException); 171 } else { 172 retVal = (BaseServerResponseException) theException; 173 } 174 175 if (retVal.getOperationOutcome() == null) { 176 retVal.setOperationOutcome(createOperationOutcome( 177 theException, theRequestDetails.getServer().getFhirContext())); 178 } 179 180 return retVal; 181 } 182 183 private IBaseOperationOutcome createOperationOutcome(Throwable theException, FhirContext ctx) 184 throws ServletException { 185 IBaseOperationOutcome oo = null; 186 if (theException instanceof BaseServerResponseException) { 187 oo = ((BaseServerResponseException) theException).getOperationOutcome(); 188 } 189 190 /* 191 * Generate an OperationOutcome to return, unless the exception throw by the resource provider had one 192 */ 193 if (oo == null) { 194 try { 195 oo = OperationOutcomeUtil.newInstance(ctx); 196 197 if (theException instanceof InternalErrorException) { 198 ourLog.error("Failure during REST processing", theException); 199 populateDetails(ctx, theException, oo); 200 } else if (theException instanceof BaseServerResponseException) { 201 int statusCode = ((BaseServerResponseException) theException).getStatusCode(); 202 203 // No stack traces for non-server internal errors 204 if (statusCode < 500) { 205 ourLog.warn("Failure during REST processing: {}", theException.toString()); 206 } else { 207 ourLog.warn("Failure during REST processing", theException); 208 } 209 210 BaseServerResponseException baseServerResponseException = 211 (BaseServerResponseException) theException; 212 populateDetails(ctx, theException, oo); 213 if (baseServerResponseException.getAdditionalMessages() != null) { 214 for (String next : baseServerResponseException.getAdditionalMessages()) { 215 OperationOutcomeUtil.addIssue(ctx, oo, "error", next, null, PROCESSING); 216 } 217 } 218 } else { 219 ourLog.error("Failure during REST processing: " + theException.toString(), theException); 220 populateDetails(ctx, theException, oo); 221 } 222 } catch (Exception e1) { 223 ourLog.error("Failed to instantiate OperationOutcome resource instance", e1); 224 throw new ServletException( 225 Msg.code(328) + "Failed to instantiate OperationOutcome resource instance", e1); 226 } 227 } else { 228 ourLog.error("Unknown error during processing", theException); 229 } 230 return oo; 231 } 232 233 private void populateDetails(FhirContext theCtx, Throwable theException, IBaseOperationOutcome theOo) { 234 if (myReturnStackTracesForExceptionTypes != null) { 235 for (Class<?> next : myReturnStackTracesForExceptionTypes) { 236 if (next.isAssignableFrom(theException.getClass())) { 237 String detailsValue = 238 theException.getMessage() + "\n\n" + ExceptionUtils.getStackTrace(theException); 239 OperationOutcomeUtil.addIssue(theCtx, theOo, "error", detailsValue, null, PROCESSING); 240 return; 241 } 242 } 243 } 244 245 OperationOutcomeUtil.addIssue(theCtx, theOo, "error", theException.getMessage(), null, PROCESSING); 246 } 247 248 /** 249 * 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 250 * issues, but may not be desirable for production situations. 251 * 252 * @param theExceptionTypes 253 * The exception types for which to return the stack trace to the user. 254 * @return Returns an instance of this interceptor, to allow for easy method chaining. 255 */ 256 public ExceptionHandlingInterceptor setReturnStackTracesForExceptionTypes(Class<?>... theExceptionTypes) { 257 myReturnStackTracesForExceptionTypes = theExceptionTypes; 258 return this; 259 } 260}