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