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