HTTP Destinations
HTTP is the most prominent communication protocol supported by the SAP Cloud SDK. Connecting to a remote system or service using HTTP is, therefore, a very common use case.
The HttpDestinationProperties
interface defines a set of properties that are required to connect to a remote system or service using HTTP, such as the URL, authentication, proxy settings, and HTTP headers.
In the following sections, we are diving into its default, and only, implementation: The DefaultHttpDestination
.
About Immutability
As per API contract, the DefaultHttpDestination
is an immutable container for destination properties.
Once initialized, it is not possible to add, remove, or modify any of its properties.
However, there might be properties that contain non-primitive values, such as lists, maps, or other objects.
These values might be mutable, depending on their implementation.
Therefore, it is impossible to guarantee true immutability for the DefaultHttpDestination
implementation.
Instead, the implementation provides a shallow immutability guarantee.
This implementation detail is important for using the builder API.
About HTTP Headers
One of the most important properties of any HttpDestination
are the HTTP headers, which are attached to any outgoing request performed with the destination.
The DefaultHttpDestination
implements support for following sources of HTTP headers:
- Properties named using the pattern
URL.header.<header-name>
will be converted into HTTP headers. - Special properties, which will be converted into HTTP headers.
- Headers that were directly added via the Builder API while assembling the
DefaultHttpDestination
instance. - Headers that are provided by
DestinationHeaderProvider
s. - Any headers required for on-premise connectivity will automatically be added, iff the
ProxyType
property is set toOnPremise
as explained below. - Headers that are dervied from authentication related properties, iff no
Authorization
header was provided by any of the previous options. - Headers that are derived from proxy authentication related properties, iff no
Proxy-Authorization
header was provided by any of the previous options.
In most cases, theProxy-Authorization
header will be derived automatically from the bound BTP Connectivity Service.
Deriving the Authorization
Header
The DefaultHttpDestination
tries to derive the Authorization
header from authentication related properties, iff no Authorization
header is provided by any other source.
In most cases, however, the BTP Destination Service will provide a Authorization
header, so this step is usually skipped.
If the destination does not stem from the BTP destination service, then the concrete properties depend on the AuthenticationType
of the DefaultHttpDestination
.
-
For
AuthenticationType.BASIC_AUTHENTICATION
, the basic credentials are taken from theUser
(orusername
, if the former doesn't exist) and thePassword
properties.
If these properties are not set, an exception is thrown. -
For
AuthenticationType.TOKEN_FORWARDING
, theRequestHeaderAccessor
is used to find anyAuthorization
header in the current request. All headers that are found (there should usually only be one) are then forwarded.
Deriving the Proxy-Authorization
Header
As described above, the DefaultHttpDestination
implements dedicated support for on-premise connectivity.
Therefore, the Proxy-Authorization
header will automatically be created as documented here iff the ProxyType
property is set to ON_PREMISE
.
Similar to the Authorization
header, the Proxy-Authentication
header is derived from proxy authentication related properties, iff no Proxy-Authentication
header is provided by any other source.
This is done by attempting to transform the value of the ProxyAuthorization
property as follows:
- If the property doesn't exist or contains an empty
String
, no header is derived. - If the property contains a
String
that starts with"Basic "
, the value is assumed to be a Base64-encodedusername:password
pair and is transformed into aBasic
header. - If the property contains a
String
that starts with"Bearer "
, the value is assumed to be a JWT token and is transformed into aBearer
header. - Any other value will be ignored and no header will be derived. Additionally, an error message will be logged.
In general, the Proxy-Authorization
header will only be derived if the properties contain a full proxy configuration.
Such a proxy configuration must contain a proxy URI, which is either taken from the Proxy
property or, if that doesn't exist, is constructed from the ProxyHost
and ProxyPort
properties.
About the Builder API
The DefaultHttpDestination.Builder
allows applications to either create a DefaultHttpDestination
instance from scratch or to "modify" an existing one.
It offers APIs to set any desired property, header, header provider, or any other value that is supported by the DefaultHttpDestination
implementation.
Provide Query Parameters for Destinations
You can add query parameters to a specific destination using properties. They will always be included in the query string when calling the target system.
Here is an example implementation which adds key1=value1
to the query string when calling the destination:
destination =
DefaultHttpDestination
.builder(server.baseUrl())
.property("URL.queries.key1", "value1")
.build();
Building on Top of an Existing Destination
Sometimes, it is needed to slightly adjust an existing Destination
instance.
For example, there might be just this one single header missing for a specific request.
Adding this header directly to the existing Destination
instance, however, is not possible, as the Destination
is immutable.
Therefore, the DefaultHttpDestination
provides a way to create a new DefaultHttpDestination
instance that is based on an existing Destination
instance.
This way is implemented in the fromDestination(Destination)
, fromProperties(DestinationProperties)
, and toBuilder()
methods.
All of these methods will return a new DefaultHttpDestination.Builder
instance, which is pre-configured with all properties of the given DestinationProperties
.
If the fromDestination
method is used with an instance of DefaultHttpDestination
, further details will be copied, such as the manually added HTTP headers and DestinationHeaderProvider
s.
The same is true for the toBuilder
method.
Example
In the following example, we are retrieving a destination using the DestinationAccessor
API.
We would like to add a new header to the destination, but we don't want to lose any of the existing headers.
Therefore, we are using the DefaultHttpDestination#fromDestination
method like so:
Destination existingDestination = DestinationAccessor.getDestination("my-destination");
assertThat(existingDestination.asHttp().getHeaders())
.containsExactly(
new Header("X-My-Header", "MyHeaderValue")
);
DefaultHttpDestination enhancedDestination = DefaultHttpDestination.fromDestination(existingDestination)
.header(new Header("My-New-Header", "MyNewHeaderValue"))
.build();
assertThat(enhancedDestination.getHeaders())
.containsExactly(
new Header("X-My-Header", "MyHeaderValue"),
new Header("My-New-Header", "MyNewHeaderValue")
);
As a result, our enhancedDestination
will contain all headers from the existingDestination
and the new My-New-Header
header.
Please note the behavior of adding individual headers and adding individual header providers.
Also, pay attention to potentially shared state between the existingDestination
and the enhancedDestination
.
Adding Individual Headers
Individual headers can be added via the header(Header)
and header(String, String)
methods.
In either case, the newly added header is only added to the list of all headers.
It will not override any existing header with the same name.
Adding Individual Header Providers
The headerProviders(DestinationHeaderProvider...)
method allows you to add as many DestinationHeaderProvider
s to the DefaultHttpDestination
instance as you wish.
Once again, existing header providers will not be overridden.
Using mTLS
The DefaultHttpDestination
can be configured to use the BTP CF Instance Identity by using SecurityConfigurationStrategy.FROM_PLATFORM
.
SecurityConfigurationStrategy.FROM_DESTINATION
, on the other hand, derives the security configuration from the destination properties.
About HeaderProviders
A DestinationHeaderProvider
is a simple, yet powerful way of adding HTTP headers to a DefaultHttpDestination
.
The interface defines just a single method:
List<Header> getHeaders( DestinationRequestContext requestContext );
If attached to a DefaultHttpDestination
, this method will be called whenever the DefaultHttpDestination
is collecting its HTTP headers (i.e. whenever the DefaultHttpDestination#getHeaders()
method is called).
The DestinationHeaderProvider
implementation hereby gets an instance of DestinationRequestContext
, which provides access to the current request URI and the destination.
Thus, the implementation gets full access to all properties of the calling DefaultHttpDestination
instance and can, therefore, decide whether and which HTTP headers it wants to add.
Calling the HttpDestination#getHeaders()
method within a DestinationHeaderProvider
implementation will result in an infinite loop and, therefore, a StackOverflowError
.
Adding Static Header Providers
In some scenarios, there are certain HTTP headers that should always be sent to a destination, regardless of the request.
Adding these headers to each and every DefaultHttpDestination
is not only cumbersome but also error-prone.
So instead of doing that, the DefaultHttpDestination
uses the Service Locator Pattern to find DestinationHeaderProvider
implementations on the classpath and adds them automatically.
Example
Lets assume you would like to always send a special X-My-Header
header to all remote systems your application is communicating with.
To do so, you would first create a DestinationHeaderProvider
implementation that adds the header to any destination:
package com.sap.cloud.sdk.cloudplatform.connectivity.example;
class MyHeaderProvider implements DestinationHeaderProvider {
@Nonnull
@Override
public List<Header> getHeaders(@Nonnull DestinationRequestContext requestContext) {
return Collections.singletonList(new Header("X-My-Header", "MyHeaderValue"));
}
}
The above code is as simple as it gets.
It will always add a static value ("MyHeaderValue"
) to every DefaultHttpDestination
.
Of course, in a real application, the logic might be a bit more complex.
To cope with that, the DestinationRequestContext
provides access to the current request URI and the destination.
That way, you could implement a more complex logic that adds different values to different destinations or decides to not add any headers, if some special condition is met.
Lastly, you would still need to make use of the Service Locator Pattern to register your DestinationHeaderProvider
implementation.
For that, you need to create a file called com.sap.cloud.sdk.cloudplatform.connectivity.DestinationHeaderProvider
in the src/main/resources/META-INF/services
folder of your application.
This file must contain the fully qualified name of your DestinationHeaderProvider
implementation.
In this example, the content should be com.sap.cloud.sdk.cloudplatform.connectivity.example.MyHeaderProvider
.
The result should look like this:
(your application root)
├── pom.xml
└── src
└── main
└── java
├── com
│ ... (truncated)
└── resources
└── META-INF
└── services
└── com.sap.cloud.sdk.cloudplatform.connectivity.DestinationHeaderProvider
Special Header Providers
The SAP Cloud SDK comes with a set of DestinationHeaderProvider
s that are automatically added to any DefaultHttpDestination
instance.
The ErpDestinationHeaderProvider
is part of the cloudplatform-connectivity
artifact and, therefore, will always be added to any DefaultHttpDestination
instance.
It provides the following headers:
- The
sap-client
header, iff theDefaultHttpDestination
contains asap-client
property. - The
sap-language
header.- If the
DefaultHttpDestination
contains a property calledcloudsdk.dynamicSapLanguage
with valuetrue
, theLocaleAccessor
will be used to determine the current locale. - Otherwise, if there is a
sap-language
property, its value will be used.
- If the