
001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2025 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.api.server.ResponseDetails; 033import ca.uhn.fhir.rest.server.RestfulServerUtils; 034import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 035import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 036import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 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 // Add headers associated with the specific error code 090 if (theException.hasResponseHeaders()) { 091 Map<String, List<String>> additional = theException.getResponseHeaders(); 092 for (Entry<String, List<String>> next : additional.entrySet()) { 093 if (isNotBlank(next.getKey()) && next.getValue() != null) { 094 String nextKey = next.getKey(); 095 for (String nextValue : next.getValue()) { 096 response.addHeader(nextKey, nextValue); 097 } 098 } 099 } 100 } 101 102 ResponseDetails responseDetails = BaseResourceReturningMethodBinding.callOutgoingFailureOperationOutcomeHook( 103 theRequestDetails, oo, theException); 104 try { 105 resetOutputStreamIfPossible(response); 106 } catch (Throwable t) { 107 ourLog.error( 108 "HAPI-FHIR was unable to reset the output stream during exception handling. The root causes follows:", 109 t); 110 } 111 112 return RestfulServerUtils.streamResponseAsResource( 113 theRequestDetails.getServer(), 114 responseDetails.getResponseResource(), 115 SUMMARY_MODE, 116 responseDetails.getResponseCode(), 117 false, 118 false, 119 theRequestDetails, 120 null, 121 null); 122 } 123 124 /** 125 * In some edge cases, the output stream is opened already by the point at which an exception is thrown. 126 * 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. 127 * Also, it strips the content-encoding header if present, as the method outcome will negotiate its own. 128 */ 129 private void resetOutputStreamIfPossible(IRestfulResponse response) { 130 if (response.getClass().isAssignableFrom(ServletRestfulResponse.class)) { 131 ServletRestfulResponse servletRestfulResponse = (ServletRestfulResponse) response; 132 HttpServletResponse servletResponse = 133 servletRestfulResponse.getRequestDetails().getServletResponse(); 134 Collection<String> headerNames = servletResponse.getHeaderNames(); 135 Map<String, Collection<String>> oldHeaders = new HashedMap<>(); 136 for (String headerName : headerNames) { 137 oldHeaders.put(headerName, servletResponse.getHeaders(headerName)); 138 } 139 140 servletResponse.reset(); 141 oldHeaders.entrySet().stream() 142 .filter(entry -> !entry.getKey().equals(CONTENT_ENCODING)) 143 .forEach(entry -> entry.getValue().stream() 144 .forEach(value -> servletResponse.addHeader(entry.getKey(), value))); 145 } 146 } 147 148 @Hook(Pointcut.SERVER_PRE_PROCESS_OUTGOING_EXCEPTION) 149 public BaseServerResponseException preProcessOutgoingException( 150 RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest) 151 throws ServletException { 152 BaseServerResponseException retVal; 153 if (theException instanceof DataFormatException) { 154 // Wrapping the DataFormatException as an InvalidRequestException so that it gets sent back to the client as 155 // a 400 response. 156 retVal = new InvalidRequestException(theException); 157 } else if (!(theException instanceof BaseServerResponseException)) { 158 retVal = new InternalErrorException(theException); 159 } else { 160 retVal = (BaseServerResponseException) theException; 161 } 162 163 if (retVal.getOperationOutcome() == null) { 164 retVal.setOperationOutcome(createOperationOutcome( 165 theException, theRequestDetails.getServer().getFhirContext())); 166 } 167 168 return retVal; 169 } 170 171 private IBaseOperationOutcome createOperationOutcome(Throwable theException, FhirContext ctx) 172 throws ServletException { 173 IBaseOperationOutcome oo = null; 174 if (theException instanceof BaseServerResponseException) { 175 oo = ((BaseServerResponseException) theException).getOperationOutcome(); 176 } 177 178 /* 179 * Generate an OperationOutcome to return, unless the exception throw by the resource provider had one 180 */ 181 if (oo == null) { 182 try { 183 oo = OperationOutcomeUtil.newInstance(ctx); 184 185 if (theException instanceof InternalErrorException) { 186 ourLog.error("Failure during REST processing", theException); 187 populateDetails(ctx, theException, oo); 188 } else if (theException instanceof BaseServerResponseException) { 189 int statusCode = ((BaseServerResponseException) theException).getStatusCode(); 190 191 // No stack traces for non-server internal errors 192 if (statusCode < 500) { 193 ourLog.warn("Failure during REST processing: {}", theException.toString()); 194 } else { 195 ourLog.warn("Failure during REST processing", theException); 196 } 197 198 BaseServerResponseException baseServerResponseException = 199 (BaseServerResponseException) theException; 200 populateDetails(ctx, theException, oo); 201 if (baseServerResponseException.getAdditionalMessages() != null) { 202 for (String next : baseServerResponseException.getAdditionalMessages()) { 203 OperationOutcomeUtil.addIssue(ctx, oo, "error", next, null, PROCESSING); 204 } 205 } 206 } else { 207 ourLog.error("Failure during REST processing: " + theException.toString(), theException); 208 populateDetails(ctx, theException, oo); 209 } 210 } catch (Exception e1) { 211 ourLog.error("Failed to instantiate OperationOutcome resource instance", e1); 212 throw new ServletException( 213 Msg.code(328) + "Failed to instantiate OperationOutcome resource instance", e1); 214 } 215 } else { 216 ourLog.error("Unknown error during processing", theException); 217 } 218 return oo; 219 } 220 221 private void populateDetails(FhirContext theCtx, Throwable theException, IBaseOperationOutcome theOo) { 222 if (myReturnStackTracesForExceptionTypes != null) { 223 for (Class<?> next : myReturnStackTracesForExceptionTypes) { 224 if (next.isAssignableFrom(theException.getClass())) { 225 String detailsValue = 226 theException.getMessage() + "\n\n" + ExceptionUtils.getStackTrace(theException); 227 OperationOutcomeUtil.addIssue(theCtx, theOo, "error", detailsValue, null, PROCESSING); 228 return; 229 } 230 } 231 } 232 233 OperationOutcomeUtil.addIssue(theCtx, theOo, "error", theException.getMessage(), null, PROCESSING); 234 } 235 236 /** 237 * 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 238 * issues, but may not be desirable for production situations. 239 * 240 * @param theExceptionTypes 241 * The exception types for which to return the stack trace to the user. 242 * @return Returns an instance of this interceptor, to allow for easy method chaining. 243 */ 244 public ExceptionHandlingInterceptor setReturnStackTracesForExceptionTypes(Class<?>... theExceptionTypes) { 245 myReturnStackTracesForExceptionTypes = theExceptionTypes; 246 return this; 247 } 248}