Skip to content

Commit 44b0d80

Browse files
authored
issue #2965 - modify getRequestBaseUri logic (#2966)
* issue #2965 - modify getRequestBaseUri logic Previously, we looked for the last instnace of `/[resourceType]` in the path and assumed that was the first thing after the baseUrl. However, if a resourceId happened to match the resource type, this proved erroneous. The updated logic looks for the first instance of `/[resourceType]/` in the path instead. If that doesn't exist, then we fall back to the old approach of using the last index of `/[resourceType]` (now that we know it should be safe to do so). This updated logic could break if someone insisted on including a baseUrl that includes a path segment that matches `/[resourceType]/` (e.g. https://example.com/my/Patient/api/ ) but I think that is an acceptable risk because that would be very dumb to do. Alternatives would be to either A. rely solely on a configured baseUrl; or B. do more processing of the URL to ensure we're stripping a path and not a hostname Signed-off-by: Lee Surprenant <lmsurpre@us.ibm.com> * issue #2965 - document known limitation for custom base urls and add a test to OriginalRequestRewriteServerTest for the exact examples used in the docs Signed-off-by: Lee Surprenant <lmsurpre@us.ibm.com>
1 parent 6503979 commit 44b0d80

10 files changed

Lines changed: 144 additions & 82 deletions

File tree

docs/src/pages/guides/FHIRServerUsersGuide.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2049,7 +2049,7 @@ This section contains reference information about each of the configuration prop
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.|
20512051
|`fhirServer/core/capabilitiesUrl`|string|The URL that is embedded in the default Capabilities statement|
2052-
|`fhirServer/core/externalBaseUrl`|string|The base URL that is embedded in the Search bundle response, as of version 4.9.0.|
2052+
|`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.|
20532053
|`fhirServer/validation/failFast`|boolean|Indicates whether validation should fail fast on create and update interactions|
20542054
|`fhirServer/term/capabilitiesUrl`|string|The URL that is embedded in the Terminology Capabilities statement using `mode=terminology`|
20552055
|`fhirServer/term/disableCaching`|boolean|Indicates whether caching is disabled for the FHIR terminology module, this includes caching in `CodeSystemSupport`, `ValueSetSupport`, `GraphTermServiceProvider`, and `RemoteTermServiceProvider`|
@@ -2720,7 +2720,7 @@ IBM FHIR Server Supports the following custom HTTP Headers:
27202720
|------------------|----------------------------|
27212721
|`X-FHIR-TENANT-ID`|Specifies which tenant config should be used for the request. Default is `default`. The header name can be overridden via config property `fhirServer/core/tenantIdHeaderName`.|
27222722
|`X-FHIR-DSID`|Specifies which datastore config should be used for the request. Default is `default`. The header name can be overridden via config property `fhirServer/core/dataSourceIdHeaderName`.|
2723-
|`X-FHIR-FORWARDED-URL`|The original (user-facing) request URL; used for constructing absolute URLs within the server response. Only enabled when explicitly configured in the default fhir-server-config.json. If either the config property or the header itself is missing, the server will use the actual request URL. The header name can be overridden via config property `fhirServer/core/originalRequestUriHeaderName`. Note, `fhirServer/core/externalBaseUrl` overrides the `X-FHIR-FORWARDED-URL` and is used to construct the absolute URL.|
2723+
|`X-FHIR-FORWARDED-URL`|The original (user-facing) request URL; used for constructing absolute URLs within the server response. Only enabled when explicitly configured in the default fhir-server-config.json. If either the config property or the header itself is missing, the server will use the actual request URL. The header name can be overridden via config property `fhirServer/core/originalRequestUriHeaderName`. Note that `fhirServer/core/externalBaseUrl` overrides the `X-FHIR-FORWARDED-URL` and is used to construct the absolute URL. Also note that the base URL's value 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.|
27242724
|`X-FHIR-UPDATE-IF-MODIFIED`|When set to true, for update and patch requests, the server will perform a resource comparison and only perform the update if the contents of the resource have changed. For all other values, the update will be executed as normal.|
27252725

27262726
# 6 Related topics

fhir-model/src/main/java/com/ibm/fhir/model/util/FHIRUtil.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,18 +259,14 @@ public static OperationOutcome buildOperationOutcome(String message, IssueType i
259259

260260
/**
261261
* Builds a relative "Location" header value for the specified resource. This will be a string of the form
262-
* <code>"<resource-type>/<id>/_history/<version>"</code>. Note that the server will turn this into an absolute URL prior to
262+
* <code>"[resource-type]/[id]/_history/[version]"</code>. Note that the server will turn this into an absolute URL prior to
263263
* returning it to the client.
264264
*
265265
* @param resource
266266
* the resource for which the location header value should be returned
267267
*/
268268
public static URI buildLocationURI(String type, Resource resource) {
269-
String resourceTypeName = resource.getClass().getSimpleName();
270-
if (!resourceTypeName.equals(type)) {
271-
resourceTypeName = type;
272-
}
273-
return URI.create(resourceTypeName + "/" + resource.getId() + "/_history/" + resource.getMeta().getVersionId().getValue());
269+
return URI.create(type + "/" + resource.getId() + "/_history/" + resource.getMeta().getVersionId().getValue());
274270
}
275271

276272
/**

fhir-persistence/src/main/java/com/ibm/fhir/persistence/FHIRPersistence.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import com.ibm.fhir.model.resource.OperationOutcome;
1414
import com.ibm.fhir.model.resource.Resource;
15-
import com.ibm.fhir.model.type.Instant;
1615
import com.ibm.fhir.persistence.context.FHIRPersistenceContext;
1716
import com.ibm.fhir.persistence.erase.EraseDTO;
1817
import com.ibm.fhir.persistence.exception.FHIRPersistenceException;
@@ -28,7 +27,7 @@ public interface FHIRPersistence {
2827

2928
/**
3029
* Stores a new FHIR Resource in the datastore. Id assignment handled by the implementation.
31-
* This method has been deprecated. Instead, generate the logical id first and use the
30+
* This method has been deprecated. Instead, generate the logical id first and use the
3231
* createWithMeta(context, resource) call instead.
3332
* @param context the FHIRPersistenceContext instance associated with the current request
3433
* @param resource the FHIR Resource instance to be created in the datastore
@@ -38,7 +37,7 @@ public interface FHIRPersistence {
3837
*/
3938
@Deprecated
4039
<T extends Resource> SingleResourceResult<T> create(FHIRPersistenceContext context, T resource) throws FHIRPersistenceException;
41-
40+
4241
/**
4342
* Stores a new FHIR Resource in the datastore. The resource is not modified before it is stored. It
4443
* must therefore already include correct Meta fields. Should be used instead of
@@ -142,7 +141,7 @@ default <T extends Resource> SingleResourceResult<T> delete(FHIRPersistenceConte
142141
* @throws FHIRPersistenceException
143142
*/
144143
MultiResourceResult<Resource> search(FHIRPersistenceContext context, Class<? extends Resource> resourceType) throws FHIRPersistenceException;
145-
144+
146145
/**
147146
* Returns true iff the persistence layer implementation supports transactions.
148147
*/
@@ -167,7 +166,7 @@ default <T extends Resource> SingleResourceResult<T> delete(FHIRPersistenceConte
167166
default boolean isDeleteSupported() {
168167
return false;
169168
}
170-
169+
171170
/**
172171
* Returns true iff the persistence layer implementation supports update/create and it has been
173172
* configured in the persistence config.

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

Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ public void assertResponse(Response response, int expectedStatusCode) {
580580
assertNotNull(response);
581581
assertEquals(expectedStatusCode, response.getStatus());
582582
}
583-
583+
584584
/**
585585
* Verify that the status code in the Response is of the expected status
586586
* family.
@@ -594,7 +594,7 @@ protected void assertResponse(Response response, Response.Status.Family family)
594594
if( ! response.getStatusInfo().getFamily().equals(family) ) {
595595
message = response.readEntity(String.class);
596596
}
597-
assertEquals(message, response.getStatusInfo().getFamily(), family);
597+
assertEquals(message, family, response.getStatusInfo().getFamily());
598598
}
599599

600600
/**
@@ -667,10 +667,24 @@ protected void assertValidationOperationOutcome(OperationOutcome oo, String msgP
667667
// Misc. common functions used by testcases.
668668
//
669669

670+
/**
671+
* Assert that the locationURI is formatted appropriately with the expected baseURL, resourceType, resourceId, and
672+
* and versionId.
673+
*
674+
* @throws Exception
675+
* @implNote Uses {@link #getRestBaseURL()} to construct the base URL for the expected location URI
676+
*/
677+
public void validateLocationURI(String locationURI, String resourceType,
678+
String expectedResourceId, String expectedVersionId) throws Exception {
679+
String expectedLocation = getRestBaseURL() + "/" + resourceType + "/"
680+
+ expectedResourceId + "/_history/" + expectedVersionId;
681+
assertEquals(expectedLocation, locationURI.toString());
682+
}
683+
670684
/**
671685
* For the specified response, this function will extract the logical id value
672686
* from the response's Location header. The format of a location header value
673-
* should be: <code>[base]/<resource-type>/<id>/_history/<version></code>
687+
* should be: <code>[base]/[resource-type]/[id]/_history/[version]</code>
674688
*
675689
* @param response the response object for a REST API invocation
676690
* @return the logical id value
@@ -734,20 +748,6 @@ private String getAbsoluteFilename(String filename) {
734748
return null;
735749
}
736750

737-
protected String[] getLocationURITokens(String locationURI) {
738-
String[] temp = locationURI.split("/");
739-
String[] tokens;
740-
if (temp.length > 4) {
741-
tokens = new String[4];
742-
for (int i = 0; i < 4; i++) {
743-
tokens[i] = temp[temp.length - 4 + i];
744-
}
745-
} else {
746-
tokens = temp;
747-
}
748-
return tokens;
749-
}
750-
751751
protected Patient setUniqueFamilyName(Patient patient, String uniqueName) {
752752
List <HumanName> nameList = new ArrayList<HumanName>();
753753
for(HumanName humanName: patient.getName()) {
@@ -787,10 +787,10 @@ public void checkForIssuesWithValidation(Resource resource, boolean failOnValida
787787
}
788788

789789
System.out.println("count = [" + issues.size() + "]");
790-
assertEquals(nonWarning,0);
790+
assertEquals(0, nonWarning);
791791

792792
if(failOnWarning) {
793-
assertEquals(allOtherIssues,0);
793+
assertEquals(0, allOtherIssues);
794794
}
795795
}
796796
else {
@@ -799,9 +799,9 @@ public void checkForIssuesWithValidation(Resource resource, boolean failOnValida
799799
}
800800

801801
/**
802-
* Parses a location URI into the resourceType, resourceId, and (optionally) the version id.
802+
* Parses a location URI into the resourceType, resource id, and version id.
803803
* @param location
804-
* @return
804+
* @return A string array with three parts: resourceType, resourceId, and versionId (in that order)
805805
*/
806806
public static String[] parseLocationURI(String location) {
807807
String[] result = null;
@@ -810,25 +810,15 @@ public static String[] parseLocationURI(String location) {
810810
}
811811

812812
String[] tokens = location.split("/");
813+
// 1 2 3 4 5 6 7 8 9 10
814+
// https: // localhost:9443 fhir-server api v4 Patient id _history version
815+
assertEquals(10, tokens.length);
816+
813817
// Check if we should expect 4 tokens or only 2.
814-
if (location.contains("_history")) {
815-
if (tokens.length >= 4) {
816-
result = new String[3];
817-
result[0] = tokens[tokens.length - 4];
818-
result[1] = tokens[tokens.length - 3];
819-
result[2] = tokens[tokens.length - 1];
820-
} else {
821-
throw new IllegalArgumentException("Incorrect location value specified: " + location);
822-
}
823-
} else {
824-
if (tokens.length >= 2) {
825-
result = new String[2];
826-
result[0] = tokens[tokens.length - 2];
827-
result[1] = tokens[tokens.length - 1];
828-
} else {
829-
throw new IllegalArgumentException("Incorrect location value specified: " + location);
830-
}
831-
}
818+
result = new String[3];
819+
result[0] = tokens[tokens.length - 4];
820+
result[1] = tokens[tokens.length - 3];
821+
result[2] = tokens[tokens.length - 1];
832822
return result;
833823
}
834824

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,42 @@ public void testWholeSystemSearch_BaseUrlRewrite() throws Exception {
9797
assertTrue(responseBundle.getEntry().stream().allMatch(e -> e.getFullUrl().getValue().startsWith(ORIGINAL_URI + "/Patient")),
9898
"All search response bundle entry fullUrls start with the passed in uri");
9999
}
100+
101+
/**
102+
* Create a Patient and ensure the Location header reflects the OriginalRequestUri
103+
*/
104+
@Test
105+
public void testCreatePatientWithCustomBaseUrls() throws Exception {
106+
WebTarget target = getWebTarget();
107+
String customBaseUrl;
108+
Patient patient = TestUtil.getMinimalResource(Patient.class);
109+
Entity<Patient> entity = Entity.entity(patient, FHIRMediaType.APPLICATION_FHIR_JSON);
110+
Response response;
111+
112+
// Build a new Patient and then call the 'create' API.
113+
customBaseUrl = "https://example.com";
114+
response = target.path("Patient").request()
115+
.header("X-FHIR-Forwarded-URL", customBaseUrl + "/Patient")
116+
.post(entity, Response.class);
117+
assertResponse(response, Response.Status.CREATED.getStatusCode());
118+
assertTrue(response.getLocation().toString().startsWith(customBaseUrl),
119+
response.getLocation().toString() + " should begin with " + customBaseUrl);
120+
121+
customBaseUrl = "https://example.com/my/patient/api";
122+
response = target.path("Patient").request()
123+
.header("X-FHIR-Forwarded-URL", customBaseUrl + "/Patient")
124+
.post(entity, Response.class);
125+
assertResponse(response, Response.Status.CREATED.getStatusCode());
126+
assertTrue(response.getLocation().toString().startsWith(customBaseUrl),
127+
response.getLocation().toString() + " should begin with " + customBaseUrl);
128+
129+
customBaseUrl = "https://example.com/my/Patient/api";
130+
response = target.path("Patient").request()
131+
.header("X-FHIR-Forwarded-URL", customBaseUrl + "/Patient")
132+
.post(entity, Response.class);
133+
assertResponse(response, Response.Status.CREATED.getStatusCode());
134+
assertFalse(response.getLocation().toString().startsWith(customBaseUrl),
135+
response.getLocation().toString() + " doesn't begin with " + customBaseUrl
136+
+ " because of a documented limitation");
137+
}
100138
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ public void testConditionalUpdateObservation2() throws Exception {
661661
String obsId = UUID.randomUUID().toString();
662662
Observation obs = TestUtil.readLocalResource("Observation1.json");
663663
obs = obs.toBuilder()
664-
.subject(Reference.builder().reference(string(fakePatientRef)).build())
664+
.subject(Reference.builder().reference(fakePatientRef).build())
665665
.build();
666666

667667
// First conditional update should find no matches, so we should get back a 201
@@ -673,11 +673,11 @@ public void testConditionalUpdateObservation2() throws Exception {
673673
String locationURI = response.getLocation();
674674
assertNotNull(locationURI);
675675

676-
String[] tokens = parseLocationURI(locationURI);
677-
String resourceId = tokens[1];
676+
// get the server-assigned resource id from the location header
677+
String resourceId = getLocationLogicalId(response.getResponse());
678678

679-
// Second conditional update should find 1 match, but because there is a un-matching
680-
// resourceId in the input resource, so we should get back a 400 error.
679+
// Second conditional update should find 1 match, but because there is an un-matching
680+
// resourceId in the input resource, we should get back a 400 error.
681681
query = new FHIRParameters().searchParam("_id", resourceId);
682682
obs = obs.toBuilder().id(obsId).build();
683683
response = client.conditionalUpdate(obs, query);

0 commit comments

Comments
 (0)