Skip to main content

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:

  1. Properties named using the pattern URL.header.<header-name> will be converted into HTTP headers.
  2. Special properties, which will be converted into HTTP headers.
  3. Headers that were directly added via the Builder API while assembling the DefaultHttpDestination instance.
  4. Headers that are provided by DestinationHeaderProviders.
  5. Any headers required for on-premise connectivity will automatically be added, iff the ProxyType property is set to OnPremise as explained below.
  6. Headers that are dervied from authentication related properties, iff no Authorization header was provided by any of the previous options.
  7. 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, the Proxy-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 the User (or username, if the former doesn't exist) and the Password properties.
    If these properties are not set, an exception is thrown.

  • For AuthenticationType.TOKEN_FORWARDING, the RequestHeaderAccessor is used to find any Authorization header in the current request. All headers that are found (there should usually only be one) are then forwarded.

Deriving the Proxy-Authorization Header

On-Premise Handling

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-encoded username:password pair and is transformed into a Basic header.
  • If the property contains a String that starts with "Bearer ", the value is assumed to be a JWT token and is transformed into a Bearer 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 DestinationHeaderProviders. 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 DestinationHeaderProviders 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.

Infinite Loop

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 DestinationHeaderProviders 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 the DefaultHttpDestination contains a sap-client property.
  • The sap-language header.
    • If the DefaultHttpDestination contains a property called cloudsdk.dynamicSapLanguage with value true, the LocaleAccessor will be used to determine the current locale.
    • Otherwise, if there is a sap-language property, its value will be used.