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