Fix #111 - Don't return stack traces in server responses y default
This commit is contained in:
parent
16857404c5
commit
8434f96e97
|
@ -4,6 +4,8 @@ import javax.servlet.ServletException;
|
|||
import javax.servlet.annotation.WebServlet;
|
||||
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor;
|
||||
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
|
@ -34,4 +36,27 @@ public class ServletExamples {
|
|||
|
||||
}
|
||||
// END SNIPPET: loggingInterceptor
|
||||
|
||||
|
||||
// START SNIPPET: exceptionInterceptor
|
||||
@WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server")
|
||||
public class RestfulServerWithExceptionHandling extends RestfulServer {
|
||||
|
||||
@Override
|
||||
protected void initialize() throws ServletException {
|
||||
|
||||
// ... define your resource providers here ...
|
||||
|
||||
// Now register the logging interceptor
|
||||
ExceptionHandlingInterceptor interceptor = new ExceptionHandlingInterceptor();
|
||||
registerInterceptor(interceptor);
|
||||
|
||||
// Return the stack trace to the client for the following exception types
|
||||
interceptor.setReturnStackTracesForExceptionTypes(InternalErrorException.class, NullPointerException.class);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
// END SNIPPET: exceptionInterceptor
|
||||
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
|||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor;
|
||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
||||
import ca.uhn.fhir.util.ReflectionUtil;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
|
@ -715,60 +716,7 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
}
|
||||
|
||||
BaseOperationOutcome oo = null;
|
||||
int statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR;
|
||||
|
||||
if (e instanceof BaseServerResponseException) {
|
||||
oo = ((BaseServerResponseException) e).getOperationOutcome();
|
||||
statusCode = ((BaseServerResponseException) e).getStatusCode();
|
||||
}
|
||||
|
||||
/*
|
||||
* Generate an OperationOutcome to return, unless the exception throw by the resource provider had one
|
||||
*/
|
||||
if (oo == null) {
|
||||
try {
|
||||
oo = (BaseOperationOutcome) myFhirContext.getResourceDefinition("OperationOutcome").getImplementingClass().newInstance();
|
||||
} catch (Exception e1) {
|
||||
ourLog.error("Failed to instantiate OperationOutcome resource instance", e1);
|
||||
throw new ServletException("Failed to instantiate OperationOutcome resource instance", e1);
|
||||
}
|
||||
|
||||
BaseIssue issue = oo.addIssue();
|
||||
issue.getSeverityElement().setValue("error");
|
||||
if (e instanceof InternalErrorException) {
|
||||
ourLog.error("Failure during REST processing", e);
|
||||
issue.getDetailsElement().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e));
|
||||
} else if (e instanceof BaseServerResponseException) {
|
||||
ourLog.warn("Failure during REST processing: {}", e);
|
||||
BaseServerResponseException baseServerResponseException = (BaseServerResponseException) e;
|
||||
statusCode = baseServerResponseException.getStatusCode();
|
||||
issue.getDetailsElement().setValue(e.getMessage());
|
||||
if (baseServerResponseException.getAdditionalMessages() != null) {
|
||||
for (String next : baseServerResponseException.getAdditionalMessages()) {
|
||||
BaseIssue issue2 = oo.addIssue();
|
||||
issue2.getSeverityElement().setValue("error");
|
||||
issue2.setDetails(next);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ourLog.error("Failure during REST processing: " + e.toString(), e);
|
||||
issue.getDetailsElement().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e));
|
||||
statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR;
|
||||
}
|
||||
} else {
|
||||
ourLog.error("Unknown error during processing", e);
|
||||
}
|
||||
|
||||
RestfulServerUtils.streamResponseAsResource(this, theResponse, oo, RestfulServerUtils.determineResponseEncodingNoDefault(theRequest), true, requestIsBrowser, NarrativeModeEnum.NORMAL,
|
||||
statusCode, false, fhirServerBase);
|
||||
|
||||
theResponse.setStatus(statusCode);
|
||||
addHeadersToResponse(theResponse);
|
||||
theResponse.setContentType("text/plain");
|
||||
theResponse.setCharacterEncoding("UTF-8");
|
||||
theResponse.getWriter().append(e.getMessage());
|
||||
theResponse.getWriter().close();
|
||||
new ExceptionHandlingInterceptor().handleException(requestDetails, e, theRequest, theResponse);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -897,7 +845,7 @@ public class RestfulServer extends HttpServlet {
|
|||
myInterceptors.add(theInterceptor);
|
||||
}
|
||||
|
||||
private boolean requestIsBrowser(HttpServletRequest theRequest) {
|
||||
public static boolean requestIsBrowser(HttpServletRequest theRequest) {
|
||||
String userAgent = theRequest.getHeader("User-Agent");
|
||||
return userAgent != null && userAgent.contains("Mozilla");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;
|
||||
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome.BaseIssue;
|
||||
import ca.uhn.fhir.rest.method.Request;
|
||||
import ca.uhn.fhir.rest.method.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.Constants;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer.NarrativeModeEnum;
|
||||
import ca.uhn.fhir.rest.server.RestfulServerUtils;
|
||||
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
|
||||
public class ExceptionHandlingInterceptor extends InterceptorAdapter {
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionHandlingInterceptor.class);
|
||||
private Class<?>[] myReturnStackTracesForExceptionTypes;
|
||||
|
||||
/**
|
||||
* 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 issues, but may
|
||||
* not be desirable for production situations.
|
||||
*
|
||||
* @param theExceptionTypes The exception types for which to return the stack trace to the user.
|
||||
* @return Returns an instance of this interceptor, to allow for easy method chaining.
|
||||
*/
|
||||
public ExceptionHandlingInterceptor setReturnStackTracesForExceptionTypes(Class<?>... theExceptionTypes) {
|
||||
myReturnStackTracesForExceptionTypes = theExceptionTypes;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleException(RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException {
|
||||
|
||||
BaseOperationOutcome oo = null;
|
||||
int statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR;
|
||||
|
||||
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
|
||||
|
||||
if (theException instanceof BaseServerResponseException) {
|
||||
oo = ((BaseServerResponseException) theException).getOperationOutcome();
|
||||
statusCode = ((BaseServerResponseException) theException).getStatusCode();
|
||||
}
|
||||
|
||||
/*
|
||||
* Generate an OperationOutcome to return, unless the exception throw by the resource provider had one
|
||||
*/
|
||||
if (oo == null) {
|
||||
try {
|
||||
oo = (BaseOperationOutcome) ctx.getResourceDefinition("OperationOutcome").getImplementingClass().newInstance();
|
||||
} catch (Exception e1) {
|
||||
ourLog.error("Failed to instantiate OperationOutcome resource instance", e1);
|
||||
throw new ServletException("Failed to instantiate OperationOutcome resource instance", e1);
|
||||
}
|
||||
|
||||
BaseIssue issue = oo.addIssue();
|
||||
issue.getSeverityElement().setValue("error");
|
||||
if (theException instanceof InternalErrorException) {
|
||||
ourLog.error("Failure during REST processing", theException);
|
||||
populateDetails(theException, issue);
|
||||
} else if (theException instanceof BaseServerResponseException) {
|
||||
ourLog.warn("Failure during REST processing: {}", theException);
|
||||
BaseServerResponseException baseServerResponseException = (BaseServerResponseException) theException;
|
||||
statusCode = baseServerResponseException.getStatusCode();
|
||||
populateDetails(theException, issue);
|
||||
if (baseServerResponseException.getAdditionalMessages() != null) {
|
||||
for (String next : baseServerResponseException.getAdditionalMessages()) {
|
||||
BaseIssue issue2 = oo.addIssue();
|
||||
issue2.getSeverityElement().setValue("error");
|
||||
issue2.setDetails(next);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ourLog.error("Failure during REST processing: " + theException.toString(), theException);
|
||||
populateDetails(theException, issue);
|
||||
statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR;
|
||||
}
|
||||
} else {
|
||||
ourLog.error("Unknown error during processing", theException);
|
||||
}
|
||||
|
||||
boolean requestIsBrowser = RestfulServer.requestIsBrowser(theRequest);
|
||||
String fhirServerBase = ((Request) theRequestDetails).getFhirServerBase();
|
||||
RestfulServerUtils.streamResponseAsResource(theRequestDetails.getServer(), theResponse, oo, RestfulServerUtils.determineResponseEncodingNoDefault(theRequest), true, requestIsBrowser,
|
||||
NarrativeModeEnum.NORMAL, statusCode, false, fhirServerBase);
|
||||
|
||||
theResponse.setStatus(statusCode);
|
||||
theRequestDetails.getServer().addHeadersToResponse(theResponse);
|
||||
theResponse.setContentType("text/plain");
|
||||
theResponse.setCharacterEncoding("UTF-8");
|
||||
theResponse.getWriter().append(theException.getMessage());
|
||||
theResponse.getWriter().close();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void populateDetails(Throwable theException, BaseIssue issue) {
|
||||
if (myReturnStackTracesForExceptionTypes != null) {
|
||||
for (Class<?> next : myReturnStackTracesForExceptionTypes) {
|
||||
if (next.isAssignableFrom(theException.getClass())) {
|
||||
issue.getDetailsElement().setValue(theException.getMessage() + "\n\n" + ExceptionUtils.getStackTrace(theException));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
issue.getDetailsElement().setValue(theException.getMessage());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package ca.uhn.fhir.rest.server;
|
||||
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -41,10 +42,14 @@ import ca.uhn.fhir.util.PortUtil;
|
|||
*/
|
||||
public class ExceptionTest {
|
||||
|
||||
private static final String OPERATION_OUTCOME_DETAILS = "OperationOutcomeDetails";
|
||||
private static CloseableHttpClient ourClient;
|
||||
private static Class<? extends Exception> ourExceptionType;
|
||||
private static boolean ourGenerateOperationOutcome;
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionTest.class);
|
||||
|
||||
private static int ourPort;
|
||||
|
||||
private static Server ourServer;
|
||||
|
||||
private static RestfulServer servlet;
|
||||
|
@ -55,21 +60,6 @@ public class ExceptionTest {
|
|||
ourExceptionType=null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThrowUnprocessableEntityWithMultipleMessages() throws Exception {
|
||||
{
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwUnprocessableEntityWithMultipleMessages=aaa");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(422, status.getStatusLine().getStatusCode());
|
||||
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent);
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("message1"));
|
||||
assertEquals(3, oo.getIssue().size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInternalError() throws Exception {
|
||||
{
|
||||
|
@ -80,10 +70,39 @@ public class ExceptionTest {
|
|||
ourLog.info(responseContent);
|
||||
assertEquals(500, status.getStatusLine().getStatusCode());
|
||||
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent);
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("InternalErrorException: Exception Text"));
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("Exception Text"));
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), not(StringContains.containsString("InternalErrorException")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInternalErrorFormatted() throws Exception {
|
||||
{
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa&_format=true");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(500, status.getStatusLine().getStatusCode());
|
||||
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent);
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("Exception Text"));
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), not(StringContains.containsString("InternalErrorException")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInternalErrorJson() throws Exception {
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa&_format=json");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(500, status.getStatusLine().getStatusCode());
|
||||
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newJsonParser().parseResource(responseContent);
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("Exception Text"));
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), not(StringContains.containsString("InternalErrorException")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResourceReturning() throws Exception {
|
||||
// No OO
|
||||
|
@ -115,30 +134,35 @@ public class ExceptionTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testInternalErrorFormatted() throws Exception {
|
||||
public void testThrowUnprocessableEntityWithMultipleMessages() throws Exception {
|
||||
{
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa&_format=true");
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwUnprocessableEntityWithMultipleMessages=aaa");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(500, status.getStatusLine().getStatusCode());
|
||||
assertEquals(422, status.getStatusLine().getStatusCode());
|
||||
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent);
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("InternalErrorException: Exception Text"));
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("message1"));
|
||||
assertEquals(3, oo.getIssue().size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInternalErrorJson() throws Exception {
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa&_format=json");
|
||||
public void testUnprocessableEntityFormatted() throws Exception {
|
||||
{
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwUnprocessableEntity=aaa&_format=true");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(500, status.getStatusLine().getStatusCode());
|
||||
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newJsonParser().parseResource(responseContent);
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("InternalErrorException: Exception Text"));
|
||||
assertEquals(UnprocessableEntityException.STATUS_CODE, status.getStatusLine().getStatusCode());
|
||||
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent);
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("Exception Text"));
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), not(StringContains.containsString("UnprocessableEntityException")));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@AfterClass
|
||||
public static void afterClass() throws Exception {
|
||||
|
@ -166,27 +190,12 @@ public class ExceptionTest {
|
|||
ourClient = builder.build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static Class<? extends Exception> ourExceptionType;
|
||||
|
||||
private static final String OPERATION_OUTCOME_DETAILS = "OperationOutcomeDetails";
|
||||
/**
|
||||
* Created by dsotnikov on 2/25/2014.
|
||||
*/
|
||||
public static class DummyPatientResourceProvider implements IResourceProvider {
|
||||
|
||||
|
||||
@Search
|
||||
public List<Patient> throwInternalError(@RequiredParam(name = "throwInternalError") StringParam theParam) {
|
||||
throw new InternalErrorException("Exception Text");
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<Patient> throwUnprocessableEntityWithMultipleMessages(@RequiredParam(name = "throwUnprocessableEntityWithMultipleMessages") StringParam theParam) {
|
||||
throw new UnprocessableEntityException("message1", "message2", "message3");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends IResource> getResourceType() {
|
||||
return Patient.class;
|
||||
|
@ -208,6 +217,21 @@ public class ExceptionTest {
|
|||
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<Patient> throwInternalError(@RequiredParam(name = "throwInternalError") StringParam theParam) {
|
||||
throw new InternalErrorException("Exception Text");
|
||||
}
|
||||
|
||||
@Search()
|
||||
public List<Patient> throwUnprocessableEntity(@RequiredParam(name = "throwUnprocessableEntity") StringParam theParam) {
|
||||
throw new UnprocessableEntityException("Exception Text");
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<Patient> throwUnprocessableEntityWithMultipleMessages(@RequiredParam(name = "throwUnprocessableEntityWithMultipleMessages") StringParam theParam) {
|
||||
throw new UnprocessableEntityException("message1", "message2", "message3");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.HttpResponse;
|
||||
|
@ -26,96 +23,87 @@ import org.junit.AfterClass;
|
|||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import ca.uhn.fhir.model.dstu.composite.HumanNameDt;
|
||||
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
|
||||
import ca.uhn.fhir.model.api.IResource;
|
||||
import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
|
||||
import ca.uhn.fhir.model.dstu.resource.Patient;
|
||||
import ca.uhn.fhir.model.dstu.valueset.IdentifierUseEnum;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.model.primitive.UriDt;
|
||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||
import ca.uhn.fhir.rest.annotation.Read;
|
||||
import ca.uhn.fhir.rest.annotation.RequiredParam;
|
||||
import ca.uhn.fhir.rest.annotation.Search;
|
||||
import ca.uhn.fhir.rest.method.RequestDetails;
|
||||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.server.ExceptionTest;
|
||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.util.PortUtil;
|
||||
|
||||
public class ExceptionHandlingInterceptorTest {
|
||||
|
||||
private static final String OPERATION_OUTCOME_DETAILS = "OperationOutcomeDetails";
|
||||
private static CloseableHttpClient ourClient;
|
||||
private static Class<? extends Exception> ourExceptionType;
|
||||
private static boolean ourGenerateOperationOutcome;
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionTest.class);
|
||||
|
||||
private static int ourPort;
|
||||
|
||||
private static Server ourServer;
|
||||
|
||||
private static RestfulServer servlet;
|
||||
private IServerInterceptor myInterceptor;
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionHandlingInterceptorTest.class);
|
||||
private static ExceptionHandlingInterceptor myInterceptor;
|
||||
|
||||
@Test
|
||||
public void testThrowUnprocessableEntityException() throws Exception {
|
||||
@Before
|
||||
public void before() {
|
||||
ourGenerateOperationOutcome = false;
|
||||
ourExceptionType=null;
|
||||
|
||||
when(myInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
when(myInterceptor.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
when(myInterceptor.handleException(any(RequestDetails.class), any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=throwUnprocessableEntityException");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
ourLog.info(IOUtils.toString(status.getEntity().getContent()));
|
||||
assertEquals(422, status.getStatusLine().getStatusCode());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
|
||||
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
|
||||
verify(myInterceptor, times(1)).handleException(any(RequestDetails.class), captor.capture(), any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
|
||||
assertEquals(UnprocessableEntityException.class, captor.getValue().getClass());
|
||||
myInterceptor.setReturnStackTracesForExceptionTypes(Throwable.class);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testThrowUnprocessableEntityExceptionAndOverrideResponse() throws Exception {
|
||||
|
||||
when(myInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
when(myInterceptor.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
|
||||
when(myInterceptor.handleException(any(RequestDetails.class), any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenAnswer(new Answer<Boolean>() {
|
||||
@Override
|
||||
public Boolean answer(InvocationOnMock theInvocation) throws Throwable {
|
||||
HttpServletResponse resp = (HttpServletResponse) theInvocation.getArguments()[3];
|
||||
resp.setStatus(405);
|
||||
resp.setContentType("text/plain");
|
||||
resp.getWriter().write("HELP IM A BUG");
|
||||
resp.getWriter().close();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=throwUnprocessableEntityException");
|
||||
public void testInternalError() throws Exception {
|
||||
myInterceptor.setReturnStackTracesForExceptionTypes(Throwable.class);
|
||||
{
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent());
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(405, status.getStatusLine().getStatusCode());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
|
||||
assertEquals("HELP IM A BUG", responseContent);
|
||||
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(500, status.getStatusLine().getStatusCode());
|
||||
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent);
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("Exception Text"));
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), (StringContains.containsString("InternalErrorException: Exception Text")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInternalErrorFormatted() throws Exception {
|
||||
myInterceptor.setReturnStackTracesForExceptionTypes(Throwable.class);
|
||||
{
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa&_format=true");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(500, status.getStatusLine().getStatusCode());
|
||||
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent);
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("Exception Text"));
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), (StringContains.containsString("InternalErrorException: Exception Text")));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@AfterClass
|
||||
public static void afterClass() throws Exception {
|
||||
ourServer.stop();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
myInterceptor = mock(IServerInterceptor.class);
|
||||
servlet.setInterceptors(Collections.singletonList(myInterceptor));
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() throws Exception {
|
||||
ourPort = PortUtil.findFreePort();
|
||||
|
@ -136,30 +124,51 @@ public class ExceptionHandlingInterceptorTest {
|
|||
builder.setConnectionManager(connectionManager);
|
||||
ourClient = builder.build();
|
||||
|
||||
myInterceptor = new ExceptionHandlingInterceptor();
|
||||
servlet.registerInterceptor(myInterceptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Created by dsotnikov on 2/25/2014.
|
||||
*/
|
||||
public static class DummyPatientResourceProvider implements IResourceProvider {
|
||||
|
||||
/**
|
||||
* Retrieve the resource by its identifier
|
||||
*
|
||||
* @param theId
|
||||
* The resource identity
|
||||
* @return The resource
|
||||
*/
|
||||
@Search(queryName = "throwUnprocessableEntityException")
|
||||
public List<Patient> throwUnprocessableEntityException() {
|
||||
throw new UnprocessableEntityException("Unprocessable!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Patient> getResourceType() {
|
||||
public Class<? extends IResource> getResourceType() {
|
||||
return Patient.class;
|
||||
}
|
||||
|
||||
@Read
|
||||
public Patient read(@IdParam IdDt theId) {
|
||||
OperationOutcome oo = null;
|
||||
if (ourGenerateOperationOutcome) {
|
||||
oo = new OperationOutcome();
|
||||
oo.addIssue().setDetails(OPERATION_OUTCOME_DETAILS);
|
||||
}
|
||||
|
||||
if (ourExceptionType == ResourceNotFoundException.class) {
|
||||
throw new ResourceNotFoundException(theId, oo);
|
||||
}else {
|
||||
throw new AssertionFailedError("Unknown exception type: " + ourExceptionType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<Patient> throwInternalError(@RequiredParam(name = "throwInternalError") StringParam theParam) {
|
||||
throw new InternalErrorException("Exception Text");
|
||||
}
|
||||
|
||||
@Search()
|
||||
public List<Patient> throwUnprocessableEntity(@RequiredParam(name = "throwUnprocessableEntity") StringParam theParam) {
|
||||
throw new UnprocessableEntityException("Exception Text");
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<Patient> throwUnprocessableEntityWithMultipleMessages(@RequiredParam(name = "throwUnprocessableEntityWithMultipleMessages") StringParam theParam) {
|
||||
throw new UnprocessableEntityException("message1", "message2", "message3");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.hamcrest.core.StringContains;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import ca.uhn.fhir.model.dstu.composite.HumanNameDt;
|
||||
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
|
||||
import ca.uhn.fhir.model.dstu.resource.Patient;
|
||||
import ca.uhn.fhir.model.dstu.valueset.IdentifierUseEnum;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.model.primitive.UriDt;
|
||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||
import ca.uhn.fhir.rest.annotation.Read;
|
||||
import ca.uhn.fhir.rest.annotation.RequiredParam;
|
||||
import ca.uhn.fhir.rest.annotation.Search;
|
||||
import ca.uhn.fhir.rest.method.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.util.PortUtil;
|
||||
|
||||
public class ExceptionInterceptorMethodTest {
|
||||
|
||||
private static CloseableHttpClient ourClient;
|
||||
private static int ourPort;
|
||||
private static Server ourServer;
|
||||
private static RestfulServer servlet;
|
||||
private IServerInterceptor myInterceptor;
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionInterceptorMethodTest.class);
|
||||
|
||||
@Test
|
||||
public void testThrowUnprocessableEntityException() throws Exception {
|
||||
|
||||
when(myInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
when(myInterceptor.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
when(myInterceptor.handleException(any(RequestDetails.class), any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=throwUnprocessableEntityException");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
ourLog.info(IOUtils.toString(status.getEntity().getContent()));
|
||||
assertEquals(422, status.getStatusLine().getStatusCode());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
|
||||
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
|
||||
verify(myInterceptor, times(1)).handleException(any(RequestDetails.class), captor.capture(), any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
|
||||
assertEquals(UnprocessableEntityException.class, captor.getValue().getClass());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testThrowUnprocessableEntityExceptionAndOverrideResponse() throws Exception {
|
||||
|
||||
when(myInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
when(myInterceptor.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
|
||||
when(myInterceptor.handleException(any(RequestDetails.class), any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenAnswer(new Answer<Boolean>() {
|
||||
@Override
|
||||
public Boolean answer(InvocationOnMock theInvocation) throws Throwable {
|
||||
HttpServletResponse resp = (HttpServletResponse) theInvocation.getArguments()[3];
|
||||
resp.setStatus(405);
|
||||
resp.setContentType("text/plain");
|
||||
resp.getWriter().write("HELP IM A BUG");
|
||||
resp.getWriter().close();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=throwUnprocessableEntityException");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent());
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(405, status.getStatusLine().getStatusCode());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
|
||||
assertEquals("HELP IM A BUG", responseContent);
|
||||
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClass() throws Exception {
|
||||
ourServer.stop();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
myInterceptor = mock(IServerInterceptor.class);
|
||||
servlet.setInterceptors(Collections.singletonList(myInterceptor));
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() throws Exception {
|
||||
ourPort = PortUtil.findFreePort();
|
||||
ourServer = new Server(ourPort);
|
||||
|
||||
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
|
||||
|
||||
ServletHandler proxyHandler = new ServletHandler();
|
||||
servlet = new RestfulServer();
|
||||
servlet.setResourceProviders(patientProvider);
|
||||
ServletHolder servletHolder = new ServletHolder(servlet);
|
||||
proxyHandler.addServletWithMapping(servletHolder, "/*");
|
||||
ourServer.setHandler(proxyHandler);
|
||||
ourServer.start();
|
||||
|
||||
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
|
||||
HttpClientBuilder builder = HttpClientBuilder.create();
|
||||
builder.setConnectionManager(connectionManager);
|
||||
ourClient = builder.build();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Created by dsotnikov on 2/25/2014.
|
||||
*/
|
||||
public static class DummyPatientResourceProvider implements IResourceProvider {
|
||||
|
||||
/**
|
||||
* Retrieve the resource by its identifier
|
||||
*
|
||||
* @param theId
|
||||
* The resource identity
|
||||
* @return The resource
|
||||
*/
|
||||
@Search(queryName = "throwUnprocessableEntityException")
|
||||
public List<Patient> throwUnprocessableEntityException() {
|
||||
throw new UnprocessableEntityException("Unprocessable!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Patient> getResourceType() {
|
||||
return Patient.class;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -158,6 +158,12 @@
|
|||
<action type="add">
|
||||
Sorting is now supported in the Web Testing UI (previously a button existed for sorting, but it didn't do anything)
|
||||
</action>
|
||||
<action type="add" issue="111">
|
||||
Server will no longer include stack traces in the OperationOutcome returned to the client
|
||||
when an exception is thrown. A new interceptor called ExceptionHandlingInterceptor has been
|
||||
created which adds this functionality back if it is needed (e.g. for DEV setups). See the
|
||||
server interceptor documentation for more information. Thanks to Andy Huang for the suggestion!
|
||||
</action>
|
||||
</release>
|
||||
<release version="0.8" date="2014-Dec-17">
|
||||
<action type="add">
|
||||
|
|
|
@ -150,6 +150,35 @@
|
|||
|
||||
</subsection>
|
||||
|
||||
<a name="ExceptionHandlingInterceptor"/>
|
||||
<subsection name="Exception Handling">
|
||||
|
||||
<p>
|
||||
The
|
||||
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.html">ExceptionHandlingInterceptor</a>
|
||||
(<a href="./xref/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.html">code</a>)
|
||||
can be used to customize what is returned to the client and what is logged when the server throws an
|
||||
exception for any reason (including routine things like UnprocessableEntityExceptions thrown as a matter of
|
||||
normal processing in a create method, but also including unexpected NullPointerExceptions thrown by client code).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The following example shows how to register an exception handling interceptor within
|
||||
a FHIR RESTful server.
|
||||
</p>
|
||||
<macro name="snippet">
|
||||
<param name="id" value="exceptionInterceptor" />
|
||||
<param name="file" value="examples/src/main/java/example/ServletExamples.java" />
|
||||
</macro>
|
||||
|
||||
<p>
|
||||
This interceptor will then produce output similar to the following:
|
||||
</p>
|
||||
<source><![CDATA[2014-09-04 02:37:30.030 Source[127.0.0.1] Operation[vread Patient/1667/_history/1] UA[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 Safari/537.36] Params[?_format=json]
|
||||
2014-09-04 03:30:00.443 Source[127.0.0.1] Operation[search-type Organization] UA[Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)] Params[]]]></source>
|
||||
|
||||
</subsection>
|
||||
|
||||
</section>
|
||||
|
||||
<section name="Creating Interceptors">
|
||||
|
|
Loading…
Reference in New Issue