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