Riptide HTTP Client

repository·main·Indexed 43 days ago

https://github.com/zalando/riptide

A next-generation Java HTTP client featuring client-side response routing and a fluent API. Riptide bridges the gap between the HTTP protocol and Java by supporting multiple return values and content negotiation. It is modular, offering specialized dependencies for Spring Boot, failsafe mechanisms (retries, circuit breakers), transient fault detection, authentication, and type-safe return value capture.

Tokens
44.4K
Snippets
121
Records
162
Agent score
56%

What's inside Riptide

  1. Overview of Riptide HTTP Client features

    main

    Riptide HTTP Client provides an alternative integration for Spring's RestTemplate (or ClientHttpRequestFactory) and Apache's HTTP Client.

    Key benefits over Spring's standard HttpComponentsClientHttpRequestFactory include:

    • Preserving the underlying client's request configuration.
    • Ensuring connections are released back to the pool after closing streams.
    • Aborting connections when the stream has not been fully consumed.
  2. Key Riptide API Entry Points

    main

    The core Riptide API is accessed through three primary entry points:

    • Http: The main entry point for making requests. Use methods like .get(), .post(), etc., to build and execute requests.
    • Navigators: A factory used to create navigators for routing logic. Common methods include series(), status(), and contentType().
    • Bindings: A factory for creating route bindings. Common methods include on(SUCCESSFUL) and anySeries().
  3. What is a Route in Riptide

    main

    A Route is the fundamental unit of response handling in Riptide. It allows you to dispatch HTTP responses to specific handlers based on response characteristics like status codes or content types. A Route can take one of two forms:

    1. A callback: Executes logic directly when the criteria are met.
    2. A nested routing tree: Traverses further into more specific routing logic (nested dispatch).

    Instead of routing requests to handlers (like a web server), Riptide routes responses to handlers on the client side.

    on(SUCCESSFUL).call(response ->
        System.out.println(response.getHeaders().getLocation()))
  4. How Routing Trees and Navigators work together

    main

    A Routing Tree is a structured way to organize multiple routes. It is composed of a Navigator and a set of Bindings.

    • Navigator: The component that decides which path to take by inspecting a specific aspect of the response (e.g., the status code or the Content-Type header).
    • Binding: Represents a specific choice offered to the navigator. It binds an attribute (like a specific status code) to a target Route.
    • Nested Dispatch: The process of traversing the tree by having a Navigator choose a Binding, which then leads to another Route or another Routing Tree.

    Example of a Routing Tree using contentType() as a navigator to branch between JSON and XML responses:

    on(SUCCESSFUL).dispatch(contentType(),
        on(APPLICATION_JSON).call(..),
        on(APPLICATION_XML).call(..))
  5. Compose multiple chaos injections

    main

    To enable multiple different failure injections simultaneously, use the composite() function within a ChaosPlugin.

    Note on Probability: Each injection evaluates its probability independently. The chance of multiple injections occurring at the same time is the product of their individual probabilities (e.g., a 1% and 0.1% chance results in a 0.001% chance of both occurring).

    new ChaosPlugin(composite(
            new LatencyInjection(
                Probability.fixed(0.01), 
                Clock.systemUTC(), 
                Duration.ofSeconds(1))),
            new ExceptionInjection(
                Probability.fixed(0.001), 
                Arrays.asList(
                    SocketTimeoutException::new,
                    NoRouteToHostException::new)))
  6. How Riptide Compression works

    main

    When a RequestCompressionPlugin is active, all request bodies are compressed using the configured method. The plugin applies the compression using chunked transfer-encoding.

    Important Constraints:

    • Single Plugin Only: You must only configure a single RequestCompressionPlugin because only one encoding can be applied at a time.
    • Spring Compatibility: Do not use RequestCompressionPlugin with Spring 4.3+ Netty4ClientHttpRequestFactory, as that factory unconditionally adds a Content-Length header which conflicts with the chunked encoding used by the plugin. Use riptide-httpclient instead.
  7. Enable Asynchronous IO in Riptide

    main

    Riptide 4+ uses blocking request factories. To achieve asynchronous behavior (where Riptide returns non-completed futures), you must register an Executor during client construction. If no executor is provided, all network communication will be synchronous, and all returned futures will be immediately completed.

    // Asynchronous: Executor + ClientHttpRequestFactory
    Http.builder()
        .executor(Executors.newCachedThreadPool())
        .requestFactory(new ApacheClientHttpRequestFactory())
        .build();
    
    // Synchronous: ClientHttpRequestFactory only
    Http.builder()
        .requestFactory(new ApacheClientHttpRequestFactory())
        .build();
  8. Work with Riptide Futures and Exceptions

    main

    Futures

    Every request returns a CompletableFuture<ClientHttpResponse>. This allows you to either chain transformations/callbacks or block on the result. For a way to capture specific return values instead of the raw response, refer to the [Riptide: Capture] documentation.

    Exceptions

    The only special custom exception you may encounter is UnexpectedResponseException. This is thrown if and only if no matching condition and no wildcard condition were found in your dispatch tree.

  9. Choose a scaling policy: queueFirst vs scaleFirst

    main

    When using an elastic pool with a work queue, you can choose how the pool reacts to new tasks:

    • queueFirst(): Tasks are first added to the queue until it is full. Only then are additional threads started. This is the standard java.util.concurrent.ThreadPoolExecutor behavior.
    • scaleFirst(): New threads are started immediately until the pool reaches its maximum size. Only after the maximum number of threads is reached does the pool start queuing tasks. This is often preferred for most applications.
  10. How retries are triggered in Riptide Failsafe

    main

    Understanding when a retry occurs is critical for correct configuration:

    1. Socket faults (e.g., read timeout): Riptide retries automatically, but only for safe and idempotent methods. Non-idempotent requests are not retried on socket timeout unless explicitly marked as idempotent.
    2. Connection faults (e.g., connection refused): If using Spring Boot auto-config with transient-fault-detection.enabled: true, these are retried for all methods. Otherwise, you must configure a retry policy for connection faults explicitly.
    3. Response-based retries: These are not automatic. A 503 response does not trigger a retry by itself. You must trigger them explicitly by:
      • Calling retry() in your routing callback.
      • Throwing a RetryException within a routing callback.

    Note on Headers: Retry-After and X-RateLimit-Reset headers influence the delay between retries once a retry has been triggered, but they do not make a response eligible for retry on their own.

  11. Extend Riptide via SPIs and core interfaces

    main

    Riptide is designed to be modular. Instead of a monolith, you can extend its functionality using several core Service Provider Interfaces (SPIs). Depending on your goal, you should implement one of the following interfaces:

    • Plugin: Use this to extend the processing pipeline for both requests and responses.
    • Route: Use this to build, compose, and reuse response handling functions.
    • Navigator: Use this to implement and build custom routing algorithms.
    • Attribute: Use this to provide public interaction points between different plugins.
  12. Understand what Riptide Micrometer measures

    main

    The MicrometerPlugin measures network communication for remote requests. It specifically excludes logic that is part of the local routing tree. For example, if a request is dispatched and a local method (like greet) is called within the handler, that local execution time is not included in the network metrics.

    // The plugin measures the network call, but excludes the execution of 'this::greet'
    http.get("/users/me")
        .dispatch(series(),
            on(SUCCESSFUL).call(User.class, this::greet),
            anySeries().call(problemHandling()))