FHIR Binary streaming uploads
Since IPF 6.0, large document uploads with ITI-65 and ITI-105 no longer need several times the document size on the heap.
How it works
A servlet filter, Base64SplittingFilter, decodes the base64 document content of
Binary.data and DocumentReference.content.attachment.data into memory or a temporary file
before the body reaches the FHIR parser, and leaves a short marker in its place. The FHIR parser
then only sees a few kilobytes of metadata.
Accessing the document content
Everything downstream of the filter sees the marker instead of the document. Consumers exchange
the marker for the content through StagedContentRegistry:
var content = StagedContentRegistry.forParameters(parameters)
.flatMap(registry -> registry.resolve(binary.getData()));
if (content.isPresent()) {
DataHandler document = content.get().dataHandler(binary.getContentType());
...
} else {
// not split: binary.getData() is the document content itself
}
Both branches are needed: a request below the size threshold is never split, so an empty
Optional is the normal case and means that the value at hand already is the document. IPF’s own
translators do not resolve markers, so a route or translator that processes the document content
has to do this itself. StagedContent also provides getSize() and getSha1Hex(), measured while
the content was streamed, so a client-supplied hash can be verified without reading the document
again.
The staged content lives only as long as the request: temporary files are deleted as soon as the response has been produced. Consume the content within the request, and do not hand it to asynchronous processing.
Configuration
The feature is off by default. In Spring Boot it is enabled and configured through the properties of the IPF Spring Boot FHIR starter:
Property (ipf.fhir.streaming-upload.) |
Default | Description |
|---|---|---|
enabled |
false |
split large uploads before they reach the FHIR parser |
engage-above-bytes |
256 KiB | only request bodies larger than this are split |
memory-threshold-bytes |
512 KiB | extracted content up to this size is kept in memory |
max-document-size-bytes |
128 MiB | maximum size of a single extracted document |
temp-directory |
JVM temp directory | directory for temporary document files |
inline-document-resources |
DocumentReference |
resource types whose create carries a document inline |
filter-order |
-90 |
position of the filter, directly after Spring Security |
Without Spring Boot
Outside Spring Boot, create the Base64SplittingFilter yourself and register it as a servlet filter
in front of the FHIR servlet. This is what the Spring Boot starter does as well. The filter extends
Spring’s OncePerRequestFilter, and ipf-commons-ihe-fhir-core declares spring-web only as an
optional dependency, so add it to your application:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
The filter takes the tuning parameters as StreamingUploadOptions, the FhirContext and the path
of the FHIR servlet below the context path. StreamingUploadOptions.defaults() yields the defaults
listed above; construct the record for other values. The filter has no default constructor, so it
cannot be declared in web.xml directly. Register it programmatically instead, e.g. in a
ServletContainerInitializer or WebApplicationInitializer:
var options = new StreamingUploadOptions(
256 * 1024, // engageAboveBytes
512 * 1024, // memoryThresholdBytes
128L * 1024 * 1024, // maxDocumentSizeBytes
Path.of("/var/tmp/ipf-uploads"), // tempDirectory
StreamingUploadOptions.MHD_INLINE_DOCUMENT_RESOURCES);
var filter = new Base64SplittingFilter(options, fhirContext, "/fhir");
servletContext.addFilter("base64SplittingFilter", filter)
.addMappingForUrlPatterns(null, false, "/*");
In a Spring application context without Spring Boot, you can declare the filter as a bean instead
and reference it from web.xml through a DelegatingFilterProxy with the same name.
A second constructor additionally takes a UnaryOperator<String>, which is applied to the
diagnostics of a rejected request before the filter sends them, and a StreamedDocumentListener,
which is notified about every document streamed out of a request body.
Mind the filter order: the filter must run after authentication, so that unauthenticated requests are never spooled to disk, and before any request logging, which then sees the slimmed-down body.
Disk space
Documents larger than memory-threshold-bytes are written to temp-directory while the request is
processed. Make sure the partition holding this directory is large enough: in the worst case it
must hold max-document-size-bytes for every document of every concurrent upload. Depending on
document sizes and load, the partition may need to be increased in size, or temp-directory
pointed to a larger one.