Skip to content

Commit 167acbd

Browse files
authored
issue 2050 ifNoneMatch returns 412 unless ifNoneMatchReturnsNotModified (#2978)
Signed-off-by: Lee Surprenant <lmsurpre@us.ibm.com>
1 parent f93ed2f commit 167acbd

11 files changed

Lines changed: 119 additions & 42 deletions

File tree

docs/src/pages/Conformance.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ The IBM FHIR Server supports a custom header, `X-FHIR-UPDATE-IF-MODIFIED`, for c
5858
The IBM FHIR Server also supports conditional create-on-update using the `If-None-Match` header. This IBM FHIR Server-specific feature allows clients to use a `PUT` (update) interaction which behaves as follows:
5959

6060
1. `If-None-Match: "*"`: If the resource does not yet exist, create the resource and return `201 Created`;
61-
2. `If-None-Match: "*"`: If the resource does exist, do nothing and return `304 Not Modified`.
61+
2. `If-None-Match: "*"`: If the resource does exist, do nothing and return `412 Precondition Failed` (default behavior);
62+
3. `If-None-Match: "*"`: If the resource does exist and the fhir-server-config element `fhirServer/core/ifNoneMatchReturnsNotModified` is set to `true`, do nothing and return `304 Not Modified`.
6263

6364
The only supported value for If-None-Match conditional create-on-update is `"*"`. This feature can also be used for `PUT` requests in transaction or batch bundles by specifying the `ifNoneMatch` field similarly in the request element:
6465
```
@@ -88,7 +89,9 @@ The only supported value for If-None-Match conditional create-on-update is `"*"`
8889
8990
```
9091

91-
If a match is found, the response bundle entry contains the location of the current resource and a status of `304` (Not Modified):
92+
If a match is found and the fhir-server-config element `fhirServer/core/ifNoneMatchReturnsNotModified` is not configured or is set to `false`, the condition is treated as an error which will cause transaction bundles to fail, returning a status of `400` (Bad Request). For batch bundles, the entry response status will be `412` (Precondition Failed).
93+
94+
If a match is found and the fhir-server-config element `fhirServer/core/ifNoneMatchReturnsNotModified` is set to `true`, the response bundle entry contains the location of the current resource and a response status of `304` (Not Modified):
9295
```
9396
{
9497
"resourceType": "Bundle",

docs/src/pages/guides/FHIRServerUsersGuide.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2048,6 +2048,7 @@ This section contains reference information about each of the configuration prop
20482048
|`fhirServer/core/defaultPageSize`|integer|Sets the page size for search and history request results when no `_count` parameter is specified.|
20492049
|`fhirServer/core/maxPageSize`|integer|Sets the maximum page size for search and history request results. If a user-specified `_count` parameter value exceeds the maximum page size, then a warning is logged and the maximum page size will be used.|
20502050
|`fhirServer/core/maxPageIncludeCount`|integer|Sets the maximum number of 'include' resources allowed per page for search and history request results. If the number of 'include' resources returned for a page of results from a search or history request will exceed the maximum number of 'include' resources allowed per page, then an error will be returned in the request results.|
2051+
|`fhirServer/core/ifNoneMatchReturnsNotModified`|boolean|When If-None-Match is specified, overrides the standard return status "412 Precondition Failed" to be "304 Not Modified". Useful in transaction bundles for clients not wanting the bundle to fail when a conflict is found.|
20512052
|`fhirServer/core/capabilitiesUrl`|string|The URL that is embedded in the default Capabilities statement|
20522053
|`fhirServer/core/externalBaseUrl`|string|The base URL that is embedded in the Search bundle response, as of version 4.9.0. Note that the base URL must not include a path segment that matches any FHIR resource type name (case-sensitive). For example, "https://example.com" or "https://example.com/my/patient/api" are fine, but "https://example.com/my/Patient/api" is not.|
20532054
|`fhirServer/validation/failFast`|boolean|Indicates whether validation should fail fast on create and update interactions|
@@ -2233,6 +2234,7 @@ This section contains reference information about each of the configuration prop
22332234
|`fhirServer/core/maxPageIncludeCount`|1000|
22342235
|`fhirServer/core/capabilitiesUrl`|null|
22352236
|`fhirServer/core/externalBaseUrl`|null|
2237+
|`fhirServer/core/ifNoneMatchReturnsNotModified`|false|
22362238
|`fhirServer/validation/failFast`|false|
22372239
|`fhirServer/term/capabilitiesUrl`|null|
22382240
|`fhirServer/term/cachingDisabled`|false|
@@ -2382,6 +2384,7 @@ must restart the server for that change to take effect.
23822384
|`fhirServer/core/extendedCodeableConceptValidation`|N|N|
23832385
|`fhirServer/core/disabledOperations`|N|N|
23842386
|`fhirServer/core/defaultPageSize`|Y|Y|
2387+
|`fhirServer/core/ifNoneMatchReturnsNotModified`|Y|Y|
23852388
|`fhirServer/core/maxPageSize`|Y|Y|
23862389
|`fhirServer/core/maxPageIncludeCount`|Y|Y|
23872390
|`fhirServer/core/capabilitiesUrl`|Y|Y|

fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class FHIRConfiguration {
3333
public static final String PROPERTY_CHECK_REFERENCE_TYPES = "fhirServer/core/checkReferenceTypes";
3434
public static final String PROPERTY_CHECK_CONTROL_CHARS = "fhirServer/core/checkControlCharacters";
3535
public static final String PROPERTY_CONDITIONAL_DELETE_MAX_NUMBER = "fhirServer/core/conditionalDeleteMaxNumber";
36+
public static final String PROPERTY_IF_NONE_MATCH_RETURNS_NOT_MODIFIED = "fhirServer/core/ifNoneMatchReturnsNotModified";
3637
public static final String PROPERTY_SERVER_REGISTRY_RESOURCE_PROVIDER_ENABLED = "fhirServer/core/serverRegistryResourceProviderEnabled";
3738
public static final String PROPERTY_SERVER_RESOLVE_FUNCTION_ENABLED = "fhirServer/core/serverResolveFunctionEnabled";
3839
public static final String PROPERTY_CAPABILITY_STATEMENT_CACHE = "fhirServer/core/capabilityStatementCacheTimeout";
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* (C) Copyright IBM Corp. 2021
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package com.ibm.fhir.persistence.exception;
8+
9+
import com.ibm.fhir.model.type.code.IssueType;
10+
import com.ibm.fhir.model.util.FHIRUtil;
11+
12+
/**
13+
* This exception is thrown when an IfNoneMatch precondition check
14+
* fails and the server is configured to treat this as an error
15+
* (412 Precondition Failed).
16+
* See FHIRConfiguration.PROPERTY_IF_NONE_MATCH_RETURNS_NOT_MODIFIED.
17+
*/
18+
public class FHIRPersistenceIfNoneMatchException extends FHIRPersistenceException {
19+
20+
// Generated serialVersionUID
21+
private static final long serialVersionUID = -6078163237462666409L;
22+
23+
public FHIRPersistenceIfNoneMatchException(String message) {
24+
super(message);
25+
withIssue(FHIRUtil.buildOperationOutcomeIssue(getMessage(), IssueType.CONFLICT));
26+
}
27+
}

fhir-server-test/src/test/java/com/ibm/fhir/server/test/BundleTest.java

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2514,7 +2514,7 @@ public void testTransactionBundleWithSkippableUpdates() throws Exception {
25142514
* Requests:
25152515
* <pre>
25162516
* 1. Transaction Bundle with PUT Patient/randomId (create-on-update - 201 Created)
2517-
* 2. Transaction Bundle with PUT Patient/randomId (skip update - 304 Not Modified)
2517+
* 2. Transaction Bundle with PUT Patient/randomId (skip update - 400 error)
25182518
* 3. DELETE Patient/randomId (deleted - 200 OK)
25192519
* 4. Transaction Bundle with PUT Patient/randomId (create-on-update - 201 Created)
25202520
* </pre>
@@ -2561,7 +2561,8 @@ public void testTransactionBundleWithIfNoneMatch() throws Exception {
25612561
assertEquals(entry1.getResponse().getLocation().getValue(), "Patient/"+randomId+"/_history/1");
25622562

25632563

2564-
// Interaction 2. PUT the same patient again. Should be skipped because IfNoneMatch
2564+
// Interaction 2. PUT the same patient again. Should be skipped because IfNoneMatch - because the
2565+
// processing error for the entry is 412, this should fail the bundle with a 400
25652566
Bundle.Entry bundleEntry2 = Bundle.Entry.builder()
25662567
.fullUrl(Uri.of("urn:2"))
25672568
.resource(patient)
@@ -2576,17 +2577,11 @@ public void testTransactionBundleWithIfNoneMatch() throws Exception {
25762577
// Process bundle
25772578
FHIRResponse response2 = client.transaction(requestBundle, returnPref);
25782579
assertNotNull(response2);
2579-
assertResponse(response2.getResponse(), Response.Status.OK.getStatusCode());
2580-
Bundle responseBundle2 = response2.getResource(Bundle.class);
2581-
printBundle("PUT", "response", responseBundle2);
2580+
assertResponse(response2.getResponse(), Response.Status.BAD_REQUEST.getStatusCode());
2581+
OperationOutcome operationOutcome2 = response2.getResource(OperationOutcome.class);
25822582

25832583
// Validate results
2584-
assertNotNull(responseBundle2);
2585-
assertEquals(responseBundle2.getEntry().size(), 1);
2586-
Bundle.Entry entry2 = responseBundle2.getEntry().get(0);
2587-
assertEquals(entry2.getResponse().getStatus().getValue(), "304");
2588-
assertEquals(entry2.getResponse().getLocation().getValue(), "Patient/"+randomId+"/_history/1");
2589-
// 304 does not include a response body, so can't check the response resource
2584+
assertNotNull(operationOutcome2);
25902585

25912586
// Interaction 3. DELETE the patient
25922587
FHIRResponse response3 = client.delete(Patient.class.getSimpleName(), randomId);
@@ -2629,10 +2624,10 @@ public void testTransactionBundleWithIfNoneMatch() throws Exception {
26292624
* multiple requests because:
26302625
* "A resource can only appear in a transaction once (by identity)."
26312626
* Requests:
2632-
* 1. Transaction Bundle with PUT Patient/randomId (create-on-update - 201 Created)
2633-
* 2. Transaction Bundle with PUT Patient/randomId (skip update - 304 Not Modified)
2627+
* 1. Batch Bundle with PUT Patient/randomId (create-on-update - 201 Created)
2628+
* 2. Batch Bundle with PUT Patient/randomId (skip update - 412 Precondition Failed)
26342629
* 3. DELETE Patient/randomId (deleted - 200 OK)
2635-
* 4. Transaction Bundle with PUT Patient/randomId (create-on-update - 201 Created)
2630+
* 4. Batch Bundle with PUT Patient/randomId (create-on-update - 201 Created)
26362631
*/
26372632
@Test
26382633
public void testBatchBundleWithIfNoneMatch() throws Exception {
@@ -2660,7 +2655,7 @@ public void testBatchBundleWithIfNoneMatch() throws Exception {
26602655

26612656
// Process bundle
26622657
FHIRRequestHeader returnPref = FHIRRequestHeader.header(PREFER_HEADER_NAME, PREFER_HEADER_RETURN_REPRESENTATION);
2663-
FHIRResponse response = client.transaction(requestBundle, returnPref);
2658+
FHIRResponse response = client.batch(requestBundle, returnPref);
26642659
assertNotNull(response);
26652660
assertResponse(response.getResponse(), Response.Status.OK.getStatusCode());
26662661
Bundle responseBundle = response.getResource(Bundle.class);
@@ -2689,7 +2684,7 @@ public void testBatchBundleWithIfNoneMatch() throws Exception {
26892684
.build();
26902685

26912686
// Process bundle
2692-
FHIRResponse response2 = client.transaction(requestBundle, returnPref);
2687+
FHIRResponse response2 = client.batch(requestBundle, returnPref);
26932688
assertNotNull(response2);
26942689
assertResponse(response2.getResponse(), Response.Status.OK.getStatusCode());
26952690
Bundle responseBundle2 = response2.getResource(Bundle.class);
@@ -2699,9 +2694,7 @@ public void testBatchBundleWithIfNoneMatch() throws Exception {
26992694
assertNotNull(responseBundle2);
27002695
assertEquals(responseBundle2.getEntry().size(), 1);
27012696
Bundle.Entry entry2 = responseBundle2.getEntry().get(0);
2702-
assertEquals(entry2.getResponse().getStatus().getValue(), "304");
2703-
assertEquals(entry2.getResponse().getLocation().getValue(), "Patient/"+randomId+"/_history/1");
2704-
// 304 does not include a response body, so can't check the response resource
2697+
assertEquals(entry2.getResponse().getStatus().getValue(), "412");
27052698

27062699
// Interaction 3. DELETE the patient
27072700
FHIRResponse response3 = client.delete(Patient.class.getSimpleName(), randomId);
@@ -2721,7 +2714,7 @@ public void testBatchBundleWithIfNoneMatch() throws Exception {
27212714
.build();
27222715

27232716
// Process bundle
2724-
FHIRResponse response4 = client.transaction(requestBundle, returnPref);
2717+
FHIRResponse response4 = client.batch(requestBundle, returnPref);
27252718
assertNotNull(response4);
27262719
assertResponse(response4.getResponse(), Response.Status.OK.getStatusCode());
27272720
Bundle responseBundle4 = response4.getResource(Bundle.class);

fhir-server-test/src/test/java/com/ibm/fhir/server/test/UpdateIfNoneMatchTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ public void testCreateOnUpdate() throws Exception {
4343
final FHIRRequestHeader ifNoneMatch = new FHIRRequestHeader(HEADERNAME_IF_NONE_MATCH, "*");
4444
response = client.update(patient, ifNoneMatch);
4545
status = response.getStatus();
46-
assertEquals(status, 304);
47-
assertEquals(response.getETag(), "W/\"1\""); // not updated
46+
47+
// by default, hitting If-None-Match is considered a 412 Precondition Failed error
48+
assertEquals(status, 412);
4849

4950
// Read back the patient and make sure it is still at version 1
5051
response = client.read(Patient.class.getSimpleName(), patientLogicalId);

fhir-server/src/main/java/com/ibm/fhir/server/resources/Update.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.ibm.fhir.exception.FHIROperationException;
3434
import com.ibm.fhir.model.resource.Resource;
3535
import com.ibm.fhir.model.type.code.IssueType;
36+
import com.ibm.fhir.persistence.exception.FHIRPersistenceIfNoneMatchException;
3637
import com.ibm.fhir.persistence.exception.FHIRPersistenceResourceNotFoundException;
3738
import com.ibm.fhir.server.spi.operation.FHIRRestOperationResponse;
3839
import com.ibm.fhir.server.util.FHIRRestHelper;
@@ -98,6 +99,10 @@ public Response update(@PathParam("type") String type, @PathParam("id") String i
9899
// By default, NOT_FOUND is mapped to HTTP 404, so explicitly set it to HTTP 405
99100
status = Status.METHOD_NOT_ALLOWED;
100101
return exceptionResponse(e, status);
102+
} catch (FHIRPersistenceIfNoneMatchException e) {
103+
// IfNoneMatch being treated as an error (default behavior)
104+
status = Status.PRECONDITION_FAILED;
105+
return exceptionResponse(e, status);
101106
} catch (FHIROperationException e) {
102107
status = issueListToStatus(e.getIssues());
103108
return exceptionResponse(e, status);

fhir-server/src/main/java/com/ibm/fhir/server/rest/FHIRRestInteractionVisitorBase.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,26 @@
1212
import static javax.servlet.http.HttpServletResponse.SC_GONE;
1313
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
1414
import static javax.servlet.http.HttpServletResponse.SC_OK;
15+
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
1516

1617
import java.net.URI;
18+
import java.util.Collections;
19+
import java.util.List;
1720
import java.util.Map;
1821
import java.util.logging.Level;
1922
import java.util.logging.Logger;
23+
import java.util.stream.Collectors;
2024

2125
import com.ibm.fhir.config.FHIRRequestContext;
2226
import com.ibm.fhir.core.HTTPReturnPreference;
2327
import com.ibm.fhir.exception.FHIROperationException;
2428
import com.ibm.fhir.model.resource.Bundle.Entry;
29+
import com.ibm.fhir.model.resource.OperationOutcome.Issue;
2530
import com.ibm.fhir.model.resource.OperationOutcome;
2631
import com.ibm.fhir.model.resource.Resource;
2732
import com.ibm.fhir.model.type.Uri;
2833
import com.ibm.fhir.model.util.ModelSupport;
34+
import com.ibm.fhir.server.exception.FHIRRestBundledRequestException;
2935
import com.ibm.fhir.server.spi.operation.FHIRResourceHelpers;
3036
import com.ibm.fhir.server.spi.operation.FHIRRestOperationResponse;
3137

@@ -42,6 +48,7 @@ public abstract class FHIRRestInteractionVisitorBase implements FHIRRestInteract
4248
protected static final com.ibm.fhir.model.type.String SC_NOT_FOUND_STRING = string(Integer.toString(SC_NOT_FOUND));
4349
protected static final com.ibm.fhir.model.type.String SC_ACCEPTED_STRING = string(Integer.toString(SC_ACCEPTED));
4450
protected static final com.ibm.fhir.model.type.String SC_OK_STRING = string(Integer.toString(SC_OK));
51+
protected static final com.ibm.fhir.model.type.String SC_PRECONDITION_FAILED_STRING = string(Integer.toString(SC_PRECONDITION_FAILED));
4552

4653
// the helper we use to do most of the heavy lifting
4754
protected final FHIRResourceHelpers helpers;
@@ -158,4 +165,22 @@ protected void addLocalRefMapping(String localIdentifier, Resource resource) {
158165
}
159166
}
160167
}
168+
169+
/**
170+
* Wrap the cause with a FHIRRestbundledRequestException and update each issue with
171+
* the entryIndex before throwing.
172+
*
173+
* @param entryIndex
174+
* @param cause
175+
* @throws FHIROperationException
176+
*/
177+
protected void updateIssuesWithEntryIndexAndThrow(Integer entryIndex, FHIROperationException cause) throws FHIROperationException {
178+
String msg = "Error while processing request bundle on entry " + entryIndex;
179+
List<Issue> updatedIssues = cause.getIssues().stream()
180+
.map(i -> i.toBuilder().expression(string("Bundle.entry[" + entryIndex + "]")).build())
181+
.collect(Collectors.toList());
182+
// no need to keep the issues in the cause any more since we've "promoted" them to the wrapped exception
183+
cause.withIssue(Collections.emptyList());
184+
throw new FHIRRestBundledRequestException(msg, cause).withIssue(updatedIssues);
185+
}
161186
}

fhir-server/src/main/java/com/ibm/fhir/server/rest/FHIRRestInteractionVisitorMeta.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -439,14 +439,4 @@ private FHIRRestOperationResponse doInteraction(int entryIndex, String requestDe
439439

440440
return null;
441441
}
442-
443-
private void updateIssuesWithEntryIndexAndThrow(Integer entryIndex, FHIROperationException cause) throws FHIROperationException {
444-
String msg = "Error while processing request bundle on entry " + entryIndex;
445-
List<Issue> updatedIssues = cause.getIssues().stream()
446-
.map(i -> i.toBuilder().expression(string("Bundle.entry[" + entryIndex + "]")).build())
447-
.collect(Collectors.toList());
448-
// no need to keep the issues in the cause any more since we've "promoted" them to the wrapped exception
449-
cause.withIssue(Collections.emptyList());
450-
throw new FHIRRestBundledRequestException(msg, cause).withIssue(updatedIssues);
451-
}
452442
}

0 commit comments

Comments
 (0)