Skip to content

Commit ebfc6af

Browse files
7385 fix 401 unauthorized response does not include operation outcome (#7389)
* Inferno test parsing failure when 401 rejection does not contain OperationOutcome - failing test * Inferno test parsing failure when 401 rejection does not contain OperationOutcome - implementation * Inferno test parsing failure when 401 rejection does not contain OperationOutcome - changelog
1 parent ceac280 commit ebfc6af

File tree

3 files changed

+30
-42
lines changed

3 files changed

+30
-42
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: fix
3+
issue: 7385
4+
jira: SMILE-11170
5+
title: "The HTTP 401 Unauthorized response now returns a FHIR OperationOutcome resource instead of a plain text message.
6+
This ensures compliance with Inferno ONC Certification expectations by resolving response body parsing failures."

hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,8 @@
4545
import ca.uhn.fhir.rest.api.server.IRestfulServer;
4646
import ca.uhn.fhir.rest.api.server.RequestDetails;
4747
import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
48-
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
4948
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
5049
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
51-
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
5250
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
5351
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
5452
import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor;
@@ -99,7 +97,6 @@
9997
import java.util.List;
10098
import java.util.ListIterator;
10199
import java.util.Map;
102-
import java.util.Map.Entry;
103100
import java.util.Set;
104101
import java.util.concurrent.locks.Lock;
105102
import java.util.concurrent.locks.ReentrantLock;
@@ -1211,23 +1208,6 @@ && isNotBlank(contentType)
12111208
hookParams.add(ServletRequestDetails.class, requestDetails);
12121209
myInterceptorService.callHooks(Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY, hookParams);
12131210

1214-
} catch (NotModifiedException | AuthenticationException e) {
1215-
1216-
unhandledException = e;
1217-
1218-
HookParams handleExceptionParams = new HookParams();
1219-
handleExceptionParams.add(RequestDetails.class, requestDetails);
1220-
handleExceptionParams.add(ServletRequestDetails.class, requestDetails);
1221-
handleExceptionParams.add(HttpServletRequest.class, theRequest);
1222-
handleExceptionParams.add(HttpServletResponse.class, theResponse);
1223-
handleExceptionParams.add(BaseServerResponseException.class, e);
1224-
if (!myInterceptorService.callHooks(Pointcut.SERVER_HANDLE_EXCEPTION, handleExceptionParams)) {
1225-
return;
1226-
}
1227-
1228-
writeExceptionToResponse(theResponse, e);
1229-
unhandledException = null;
1230-
12311211
} catch (Throwable e) {
12321212

12331213
unhandledException = e;
@@ -2075,26 +2055,6 @@ private void unregisterAllProviders(List<?> theProviders) {
20752055
}
20762056
}
20772057

2078-
private void writeExceptionToResponse(HttpServletResponse theResponse, BaseServerResponseException theException)
2079-
throws IOException {
2080-
theResponse.setStatus(theException.getStatusCode());
2081-
addHeadersToResponse(theResponse);
2082-
if (theException.hasResponseHeaders()) {
2083-
for (Entry<String, List<String>> nextEntry :
2084-
theException.getResponseHeaders().entrySet()) {
2085-
for (String nextValue : nextEntry.getValue()) {
2086-
if (isNotBlank(nextValue)) {
2087-
theResponse.addHeader(nextEntry.getKey(), nextValue);
2088-
}
2089-
}
2090-
}
2091-
}
2092-
theResponse.setContentType("text/plain");
2093-
theResponse.setCharacterEncoding("UTF-8");
2094-
String message = UrlUtil.sanitizeUrlPart(theException.getMessage());
2095-
theResponse.getWriter().write(message);
2096-
}
2097-
20982058
/**
20992059
* By default, server create/update/patch/transaction methods return a copy of the resource
21002060
* as it was stored. This may be overridden by the client using the

hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerExceptionDstu3Test.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.List;
4141

4242
import static org.assertj.core.api.Assertions.assertThat;
43+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
4344
import static org.junit.jupiter.api.Assertions.assertEquals;
4445

4546
public class ServerExceptionDstu3Test {
@@ -187,22 +188,43 @@ public void testPostWithNoBody() throws IOException {
187188
}
188189

189190

191+
/**
192+
* Ensures that when an {@link ca.uhn.fhir.rest.server.exceptions.AuthenticationException} is handled and
193+
* an <b>HTTP 401 Unauthorized</b> response is generated, the server returns a FHIR
194+
* {@code OperationOutcome} rather than plain text. This prevents JSON parse failures in
195+
* ONC Inferno’s SMART v2 tests.
196+
* <p>
197+
* These parse failures were observed in the <b>Granular Scopes 2</b> section, specifically in:
198+
* <ul>
199+
* <li>Server filters results for Condition reads based on granular scopes</li>
200+
* <li>Server filters results for Observation reads based on granular scopes</li>
201+
* </ul>
202+
* For details, see: <a href="https://github.com/hapifhir/hapi-fhir/issues/7385">#7385</a>
203+
*/
190204
@Test
191205
public void testAuthorize() throws Exception {
192-
206+
// setup
193207
OperationOutcome operationOutcome = new OperationOutcome();
194208
operationOutcome.addIssue().setCode(IssueType.BUSINESSRULE);
195-
196209
ourException = new AuthenticationException().addAuthenticateHeaderForRealm("REALM");
197210

211+
// execute
198212
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/Patient");
199213
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
200214
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
201215
ourLog.info(status.getStatusLine().toString());
202216
ourLog.info(responseContent);
203217

218+
// validate
204219
assertEquals(401, status.getStatusLine().getStatusCode());
205220
assertEquals("Basic realm=\"REALM\"", status.getFirstHeader("WWW-Authenticate").getValue());
221+
OperationOutcome outcome = assertDoesNotThrow(() ->
222+
ourCtx.newXmlParser().parseResource(OperationOutcome.class, responseContent));
223+
assertThat(outcome.getIssue()).hasSize(1);
224+
OperationOutcome.OperationOutcomeIssueComponent issue = outcome.getIssueFirstRep();
225+
assertEquals(OperationOutcome.IssueSeverity.ERROR, issue.getSeverity());
226+
assertEquals(OperationOutcome.IssueType.PROCESSING, issue.getCode());
227+
assertEquals("Client unauthorized", issue.getDiagnostics());
206228
}
207229

208230
}

0 commit comments

Comments
 (0)