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}