HTTP Quickstart

Integrating to an external system via HTTP is slightly different when compared to JMS or Kafka. This guide applies to outbound integration (i.e. where IPF makes requests to an external system), for inbound integration via HTTP (i.e. where IPF receives requests from another system) please refer to HTTP Receiving Flow Quickstart.

For an example of using the HTTP Connector Transport to integrate with the Twitter API, see Chained Request-Reply with OAuth.

When implementing an outbound integration via HTTP, instead of defining a pair of sending and receiving connectors a RequestReplySendConnector is required instead. This single connector makes the request and also handles the response from the external system asynchronously.

Step 1: Add connector-http dependency

The dependency to add to your pom.xml is:

<dependency>
    <groupId>com.iconsolutions.ipf.core.connector</groupId>
    <artifactId>connector-http</artifactId>
</dependency>

If importing the Icon BOM, or using the Icon BOM as a parent, there’s no need to supply a separate version.

Step 2: Config

Connector configuration - in general - is heavily config-driven.The configuration allows us to specify:

  • HTTP server host/port

  • Proxy settings

  • Restart settings (on failure)

Here’s an example of a configuration block for a HttpConnectorTransportConfiguration which we will wire into our ConnectorTransport, for a fictional bank’s fraud system which exposes a REST endpoint:

bank.fraud-system { (1)
  http {
    client {
      host = "olaf" (2)
      endpoint-url = "/test" (3)
      proxy-host = "localhost" (4)
      ssl {
        trust-store-location = "target/test-classes/tls/client/trust.p12" (5)
        trust-store-password = "password" (6)
        trust-store-type = "PKCS12" (7)
        key-store-location = "target/test-classes/tls/client/keys.p12" (8)
        key-store-password = "password" (9)
        key-store-type = "PKCS12" (10)
        key-password = "password" (11)
      }
      buffer-size = 1
      concurrency = 1
      status-codes-treated-as-errors = [404, 5xx] (12)
      restart-settings { (13)
        min-backoff = 1s
        max-backoff = 30s
        random-factor = 0.2
        max-restarts = 200
        max-restarts-within = 12h
      }
      connection-pool { (14)
        max-connections = 5 (15)
      }
    }
  }
}
1 This is known as the config root path and will be referenced in the code. It indicates where in the application’s configuration to look for this Send Connector Transport’s settings
2 The hostname of the HTTP service
3 The route of the endpoint within the service
4 Hostname of the proxy, if it is used
5 A path to the file location of the truststore
6 Passphrase to access the keystore
7 The type of truststore to use. Should be one of PKCS12 or JKS
8 A path to the file location of the keystore
9 Passphrase to access the keystore
10 The type of keystore to use. Should be one of PKCS12 or JKS
11 Passphrase to access the keystore
12 Status codes that will be treated as errors. Standard status code patterns in form of 4xx and 5xx are supported as well.
13 Optional configuration block which will override the default stream RestartSettings. See: doc.akka.io/libraries/akka-core/current//stream/stream-error.html#delayed-restarts-with-a-backoff-operator for more details. Note that this is only used in the HttpConnectorTransportConfiguration.
14 Optional configuration block which will override Akka’s default connection pool config
15 Custom value for max number of connections, as example. Detailed attributes list with defaults and description could be found at doc.akka.io/docs/akka-http/current/client-side/configuration.html#pool-settings

Step 3: Create HTTP Connector Transport Configuration

You can skip this step by providing configRootPath to the HttpConnectorTransport.Builder<T> instead of HttpTransportConfiguration.

This example achieves the same thing as an example in Step 4:

var connectorTransport = new HttpConnectorTransport.Builder<T>()
                .withActorSystem(actorSystem)
                .withName("exampleHttpConnectorTransport")
                .withConfigRootPath("bank.fraud-system")
                .withTreatErrorResponseAsFailureWhen(httpResponse -> {
                    if(httpResponse.status().intValue() == 505) {
                        return false;
                    }
                    return true;
                })
                .build();

The HTTP Connector Transport Configuration class acts as a wrapper for the HOCON configuration which we defined in Step 2, the application accesses the configuration via this class.

Here’s how to create an HTTP Connector Transport Configuration class for configuring an HTTP Connector Transport:

var connectorTransportConfiguration = HttpConnectorTransportConfiguration.create(actorSystem.classicSystem().settings().config(), (1)
"bank.fraud-system"); (2)
1 This retrieves the application configuration from the actor system. The actorSystem can typically be injected as a Spring bean.
2 This indicates the path to the config where the application should retrieve configuration values from.

Step 4: Create HTTP Connector Transport

Here’s an example of how a HttpConnectorTransport can be created:

var connectorTransport = new HttpConnectorTransport.Builder<T>() (1)
                .withName("exampleHttpConnectorTransport") (2)
                .withActorSystem(actorSystem)
                .withConfigRootPath("example-transport-configuration") (3)
                .withTreatErrorResponseAsFailureWhen(httpResponse -> { (4)
                    if(httpResponse.status().intValue() == 505) {
                        return false;
                    }
                    return true;
                })
                .build();
1 Replace T with the target type you are sending
2 Give the ConnectorTransport a meaningful name
3 Provide a configuration root path to your HTTP connector transport configuration (see Step 3 for an example)
4 You can optionally customize how to handle specific HTTP Response codes. Takes a Predicate<HttpResponse>, in the example we are going treat a 505 response code as a success response. This could be used in conjunction with Resilience settings to customize retry behaviour. For example, by not treating a response as a failure you can bypass default retry handling. This setup can be avoided if we set status-codes-treated-as-errors parameter in our config root path. Statuses we set there will be treated as errors, and ones not in the list won’t.
if you set both withTreatErrorResponseAsFailureWhen and use the status-codes-treated-as-errors configuration, the latter will be ignored. If you are simply checking HTTP status codes for success or failure, you can just use the configuration key without having to write any code.
If the endpoint being connected to utilises Bearer Authentication, refer to the Bearer Authentication guide here

Step 5: Create HTTP Receive Connector Transport

Here’s an example of how a HttpReceiveConnectorTransport can be created:

var configRootPath = "receive-example"; (1)
var config = actorSystem.classicSystem().settings().config().getConfig(configRootPath + ".http.receive");
var connectorTransport =  HttpReceiveConnectorTransport.builder()
        .withName("exampleHttpConnectorTransport") (2)
        .withActorSystem(actorSystem)
        .withHost(config.getString("host"))
        .withPort(config.getString("port"))
        .withSslConfiguration(HttpReceiveTransportConfiguration.SSLConfig.buildSslContextFromConfiguration(config))
        .withResponder(req -> HttpResponse.create().withStatus(202))
        .withConfigRootPath(configRootPath)
        .build();
1 Set configRootPath
2 Give the ConnectorTransport a meaningful name

Step 6: Create HTTP Request Reply Send Connector

Here is an example of how a RequestReplySendConnector can be created:

var connectorConfigRootPath = "ipf.example-project.transport.connector";
var transportRootPath = "ipf.example-project.transport";
var resiliencySettingsRootPath = connectorConfigRootPath + ".resiliency-settings";

var rrSendConnector = new RequestReplySendConnector.Builder<REQ_D, REQ_T, REP_D, REP_T>(
        "RRSendConnector", connectorConfigRootPath, actorSystem)
        .withConnectorTransport(new HttpConnectorTransport.Builder<REQ_D>("RRSendConnectorTransport", actorSystem, transportRootPath).build())
        .withMessageLogger(messageLogger)
        .withCorrelationIdExtractor(payload -> null)
        .withReceiveTransportMessageConverter(t -> convertToDomain(t.getPayload().toString()))
        .withTargetToDomainTypeConverter(payload -> payload)
        .withSendTransportMessageConverter(settingsApiGetAllTransportMessageConvertor)
        .build();

Step 7: Send Messages

Having created a send connector instance, it can now be used to send messages to the target endpoint, like so:

rrSendConnector.send(PROCESSING_CONTEXT, message) (1)
        .handle((response, e) -> { (2)
            if (response != null) {
                return ... // successful case
            }

            if (ExceptionUtils.getRootCause(e) instanceof HttpStatusErrorException se) { (3)
                var transportMessage = se.getResponseMessage(); (4)
                var errorPayload = transportMessage.getPayload().toString();
                var mappedResponse = ... (5)
                return mappedResponse;
            } else {
                ... (6)
            }
        });
1 Sending messages using HTTP and a Request Reply Send Connector starts the same as with the asynchronous send connectors that use JMS or Kafka.
2 Unlike JMS or Kafka, where messages that indicate business errors are sent to a pre-allocated response queue/topic, here the business errors will be returned instantly via the HTTP response. HTTP responses that contain status codes that are treated as errors will result in failed CompletableFutures being returned by the Request Reply connector. Here we are accessing the error that caused the future to fail via a handle method but exceptionally, whenComplete or any of their alternatives would also work.
3 The exceptions failing the futures will all have as their root cause an instance of HttpStatusErrorException.
4 HttpStatusErrorException carries within it a TransportMessage representing the received error response.
5 TransportMessage only carries the raw response payload, which is of String or byte[] type. That is why a mapping step will almost always be needed in your exception handlers.
6 Your future can fail with a non-HttpStatusErrorException and these have to be handled somehow as well.