Fix #111 - Don't return stack traces in server responses y default

This commit is contained in:
James Agnew 2015-03-11 17:18:42 -04:00
parent 16857404c5
commit 8434f96e97
8 changed files with 511 additions and 186 deletions

View File

@ -4,6 +4,8 @@ import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet; import javax.servlet.annotation.WebServlet;
import ca.uhn.fhir.rest.server.RestfulServer; 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; import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ -34,4 +36,27 @@ public class ServletExamples {
} }
// END SNIPPET: loggingInterceptor // 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
} }

View File

@ -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.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; 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.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.util.ReflectionUtil; import ca.uhn.fhir.util.ReflectionUtil;
import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.UrlUtil;
@ -715,61 +716,8 @@ public class RestfulServer extends HttpServlet {
} }
} }
BaseOperationOutcome oo = null; new ExceptionHandlingInterceptor().handleException(requestDetails, e, theRequest, theResponse);
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();
} }
} }
@ -897,7 +845,7 @@ public class RestfulServer extends HttpServlet {
myInterceptors.add(theInterceptor); myInterceptors.add(theInterceptor);
} }
private boolean requestIsBrowser(HttpServletRequest theRequest) { public static boolean requestIsBrowser(HttpServletRequest theRequest) {
String userAgent = theRequest.getHeader("User-Agent"); String userAgent = theRequest.getHeader("User-Agent");
return userAgent != null && userAgent.contains("Mozilla"); return userAgent != null && userAgent.contains("Mozilla");
} }

View File

@ -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());
}
}

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.rest.server; package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.util.List; import java.util.List;
@ -41,35 +42,24 @@ import ca.uhn.fhir.util.PortUtil;
*/ */
public class ExceptionTest { public class ExceptionTest {
private static final String OPERATION_OUTCOME_DETAILS = "OperationOutcomeDetails";
private static CloseableHttpClient ourClient; private static CloseableHttpClient ourClient;
private static Class<? extends Exception> ourExceptionType;
private static boolean ourGenerateOperationOutcome; private static boolean ourGenerateOperationOutcome;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionTest.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionTest.class);
private static int ourPort; private static int ourPort;
private static Server ourServer; private static Server ourServer;
private static RestfulServer servlet; private static RestfulServer servlet;
@Before @Before
public void before() { public void before() {
ourGenerateOperationOutcome = false; ourGenerateOperationOutcome = false;
ourExceptionType=null; 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 @Test
public void testInternalError() throws Exception { public void testInternalError() throws Exception {
{ {
@ -80,10 +70,39 @@ public class ExceptionTest {
ourLog.info(responseContent); ourLog.info(responseContent);
assertEquals(500, status.getStatusLine().getStatusCode()); assertEquals(500, status.getStatusLine().getStatusCode());
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent); 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 @Test
public void testResourceReturning() throws Exception { public void testResourceReturning() throws Exception {
// No OO // No OO
@ -115,31 +134,36 @@ public class ExceptionTest {
} }
@Test @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); HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent()); String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent); ourLog.info(responseContent);
assertEquals(500, status.getStatusLine().getStatusCode()); assertEquals(422, status.getStatusLine().getStatusCode());
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent); 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 @Test
public void testInternalErrorJson() throws Exception { public void testUnprocessableEntityFormatted() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa&_format=json"); {
HttpResponse status = ourClient.execute(httpGet); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwUnprocessableEntity=aaa&_format=true");
String responseContent = IOUtils.toString(status.getEntity().getContent()); HttpResponse status = ourClient.execute(httpGet);
IOUtils.closeQuietly(status.getEntity().getContent()); String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info(responseContent); IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(500, status.getStatusLine().getStatusCode()); ourLog.info(responseContent);
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newJsonParser().parseResource(responseContent); assertEquals(UnprocessableEntityException.STATUS_CODE, status.getStatusLine().getStatusCode());
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("InternalErrorException: Exception Text")); 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 @AfterClass
public static void afterClass() throws Exception { public static void afterClass() throws Exception {
ourServer.stop(); ourServer.stop();
@ -166,27 +190,12 @@ public class ExceptionTest {
ourClient = builder.build(); ourClient = builder.build();
} }
private static Class<? extends Exception> ourExceptionType;
private static final String OPERATION_OUTCOME_DETAILS = "OperationOutcomeDetails";
/** /**
* Created by dsotnikov on 2/25/2014. * Created by dsotnikov on 2/25/2014.
*/ */
public static class DummyPatientResourceProvider implements IResourceProvider { 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 @Override
public Class<? extends IResource> getResourceType() { public Class<? extends IResource> getResourceType() {
return Patient.class; 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");
}
} }
} }

View File

@ -1,16 +1,13 @@
package ca.uhn.fhir.rest.server.interceptor; package ca.uhn.fhir.rest.server.interceptor;
import static org.junit.Assert.*; import static org.hamcrest.Matchers.not;
import static org.mockito.Mockito.*; 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.List;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest; import junit.framework.AssertionFailedError;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
@ -26,96 +23,87 @@ import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; 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.api.IResource;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt; import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu.resource.Patient; 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.IdDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search; 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.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer; 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.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.PortUtil;
public class ExceptionHandlingInterceptorTest { public class ExceptionHandlingInterceptorTest {
private static final String OPERATION_OUTCOME_DETAILS = "OperationOutcomeDetails";
private static CloseableHttpClient ourClient; 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 int ourPort;
private static Server ourServer; private static Server ourServer;
private static RestfulServer servlet; private static RestfulServer servlet;
private IServerInterceptor myInterceptor; private static ExceptionHandlingInterceptor myInterceptor;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionHandlingInterceptorTest.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());
}
@Before
@Test public void before() {
public void testThrowUnprocessableEntityExceptionAndOverrideResponse() throws Exception { 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))).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);
myInterceptor.setReturnStackTracesForExceptionTypes(Throwable.class);
} }
@Test
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());
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")));
}
}
@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 @AfterClass
public static void afterClass() throws Exception { public static void afterClass() throws Exception {
ourServer.stop(); ourServer.stop();
} }
@Before
public void before() {
myInterceptor = mock(IServerInterceptor.class);
servlet.setInterceptors(Collections.singletonList(myInterceptor));
}
@BeforeClass @BeforeClass
public static void beforeClass() throws Exception { public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort(); ourPort = PortUtil.findFreePort();
@ -136,30 +124,51 @@ public class ExceptionHandlingInterceptorTest {
builder.setConnectionManager(connectionManager); builder.setConnectionManager(connectionManager);
ourClient = builder.build(); ourClient = builder.build();
myInterceptor = new ExceptionHandlingInterceptor();
servlet.registerInterceptor(myInterceptor);
} }
/** /**
* Created by dsotnikov on 2/25/2014. * Created by dsotnikov on 2/25/2014.
*/ */
public static class DummyPatientResourceProvider implements IResourceProvider { 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 @Override
public Class<Patient> getResourceType() { public Class<? extends IResource> getResourceType() {
return Patient.class; 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");
}
} }
} }

View File

@ -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;
}
}
}

View File

@ -158,6 +158,12 @@
<action type="add"> <action type="add">
Sorting is now supported in the Web Testing UI (previously a button existed for sorting, but it didn't do anything) Sorting is now supported in the Web Testing UI (previously a button existed for sorting, but it didn't do anything)
</action> </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>
<release version="0.8" date="2014-Dec-17"> <release version="0.8" date="2014-Dec-17">
<action type="add"> <action type="add">

View File

@ -150,8 +150,37 @@
</subsection> </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>
<section name="Creating Interceptors"> <section name="Creating Interceptors">
<p> <p>