Spring Boot Skills
GitHub
Framework 7 Core

HTTP Interface Clients

Use when calling external HTTP APIs from Spring Boot 4 / Spring Framework 7 with declarative HttpExchange interfaces. Covers ImportHttpServices, grouped base URLs and timeouts, and RestClient versus WebClient selection.

View SKILL.md

Define a remote API as an interface with @HttpExchange methods. Boot 4 registers the proxy with @ImportHttpServices, so do not create a manual factory for every client.

Define and register the interface

@HttpExchange("/orders")
public interface OrderApiClient {
    @GetExchange("/{id}")
    OrderDto get(@PathVariable UUID id);

    @PostExchange
    OrderDto create(@RequestBody CreateOrderRequest request);
}

@SpringBootApplication
@ImportHttpServices(group = "orders", basePackages = "com.example.client.orders")
public class Application { }

Inject OrderApiClient like any other bean. The interface needs no @Component and no implementation class.

Configure groups and transport

spring:
  http:
    clients:
      connect-timeout: 2s
      read-timeout: 5s
    serviceclient:
      orders:
        base-url: https://orders.internal.example.com
        read-timeout: 10s

spring.http.clients.* contains global transport defaults. spring.http.serviceclient.<group>.* contains per-group settings, and the group name must match @ImportHttpServices.

The default client type is blocking RestClient. Select HttpServiceGroup.ClientType.WEB_CLIENT for interfaces returning Mono or Flux, and include the WebClient starter. Group configurers apply to a group, not one interface.

Manual HttpServiceProxyFactory wiring remains valid for one-off clients, but it is the Boot 3 pattern and should not be the default in Boot 4.

Transport failure contract

Define an adapter policy before adding retry annotations. The companion CatalogHttpClient implements the CatalogRetryService.CatalogTransport port from the resilience-retry skill and uses explicit JDK connect/read timeouts. It is a small imperative adapter example, not a reason to replace an existing HTTP interface or Boot-managed client.

Failure Example policy for an idempotent read
401/403, malformed request, missing resource Surface the domain-appropriate error; no blind retry
502/503/504, connect/read timeout Translate to the bounded retry policy
429 with Retry-After Schedule only within the caller's budget; not implemented by this example
Cancellation or interruption Propagate immediately, preserving interruption
Invalid response body or TLS/certificate error Diagnose; do not repeatedly retry by default

For POST/payment calls, preserve the same idempotency key and request bytes across retries; a timeout does not prove the server did nothing. Never forward inbound Authorization to an arbitrary destination: configure credentials per trusted service. Do not allow an end user to choose the base URL. Avoid logging tokens or raw upstream error bodies.

Test the real client adapter against a stub HTTP server, including permanent status codes, transient-then-success responses, malformed bodies, delayed responses and cancellation. Keep connect, read, pool-acquisition and total deadlines distinct. When using WebClient, apply a total timeout around the whole retry sequence and avoid blocking the event loop.

See Spring REST clients.

Verification scope

The repository fixture targets Boot 4.1.0 with Java 17/21 and checks the imperative catalog adapter against a local HTTP server; not every declarative group-registration option. Check the coverage matrix before treating another minor version or an illustrative snippet as verified.

Gotchas

  • Agent creates a HttpServiceProxyFactory bean for every client - use @ImportHttpServices and groups.
  • Agent adds @Component or an implementation to the interface - generated proxies need neither.
  • Agent uses spring.http.client.* - global defaults are under spring.http.clients.*; group settings use serviceclient.
  • Agent hard-codes a host in @HttpExchange and sets a group base URL - keep the host in configuration.
  • Agent returns Mono or Flux with the default client - select WEB_CLIENT explicitly.
  • Agent assumes a group configurer customizes one interface - configure the group or separate the group.
FROM THE REPOSITORY

Examples & templates

bad-order-client.javaexamples
Source on GitHub
@Component
@HttpExchange("https://orders.internal/orders")
interface OrderClient {
    Mono<OrderDto> get(UUID id); // default group client is blocking
}
good-order-client.javaexamples
Source on GitHub
@HttpExchange("/orders")
interface OrderClient {
    @GetExchange("/{id}") OrderDto get(@PathVariable UUID id);
}

@Configuration(proxyBeanMethods = false)
@ImportHttpServices(group = "orders", types = OrderClient.class)
class OrderClientConfiguration { }
CatalogHttpClient.javatemplates
Source on GitHub
package com.example.clients;

import java.net.ConnectException;
import java.net.URI;
import java.net.SocketTimeoutException;
import java.net.http.HttpClient;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClient;
import com.example.clients.CatalogRetryService.CatalogTransport;
import com.example.clients.CatalogRetryService.TransientCatalogException;

/** Error classification belongs in the transport adapter, not in controllers. */
public final class CatalogHttpClient implements CatalogTransport {
    private final RestClient client;
    public CatalogHttpClient(RestClient client) { this.client = client; }

    public static CatalogHttpClient create(URI baseUrl, Duration connectTimeout, Duration readTimeout) {
        if (connectTimeout == null || readTimeout == null || connectTimeout.isNegative()
                || connectTimeout.isZero() || readTimeout.isNegative() || readTimeout.isZero()) {
            throw new IllegalArgumentException("Positive transport timeouts required");
        }
        var http = HttpClient.newBuilder().connectTimeout(connectTimeout).build();
        var factory = new JdkClientHttpRequestFactory(http);
        factory.setReadTimeout(readTimeout);
        return new CatalogHttpClient(RestClient.builder().baseUrl(baseUrl.toString()).requestFactory(factory).build());
    }

    @Override public String fetch(UUID id) {
        try {
            return client.get().uri("/catalog/{id}", id).retrieve().body(String.class);
        } catch (HttpServerErrorException failure) {
            int status = failure.getStatusCode().value();
            if (status == 502 || status == 503 || status == 504) {
                throw new TransientCatalogException(failure);
            }
            throw failure;
        } catch (ResourceAccessException failure) {
            boolean transientFailure = false;
            for (Throwable cause = failure; cause != null; cause = cause.getCause()) {
                if (cause instanceof InterruptedException) {
                    Thread.currentThread().interrupt();
                    throw new CancellationException("Interrupted");
                }
                transientFailure |= cause instanceof ConnectException
                    || cause instanceof SocketTimeoutException || cause instanceof HttpTimeoutException;
            }
            if (transientFailure) throw new TransientCatalogException(failure);
            throw failure;
        }
    }
}

Verification scope

Compiled sources
CatalogHttpClient, both trees
Runtime checks
Real stub HTTP server: transient/permanent status classification and read timeout

Coverage describes checked-in fixtures, not a guarantee for every snippet or project.

Coverage matrix & agent evidence
ADD TO YOUR AGENT

Install Boot 4 skills

claude plugin marketplace add rrezartprebreza/spring-boot-skills
claude plugin install spring-boot-4-skills@spring-boot-skills