CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-org-springframework-ai--spring-ai-autoconfigure-retry

Spring Boot auto-configuration for AI retry capabilities with exponential backoff and intelligent HTTP error handling

Overview
Eval results
Files

auto-configuration.mddocs/reference/

Auto-Configuration

Spring Boot auto-configuration for AI retry capabilities. The auto-configuration automatically creates and configures retry beans when the library is detected on the classpath.

Capabilities

SpringAiRetryAutoConfiguration

Main auto-configuration class that provides retry beans.

// Package: org.springframework.ai.retry.autoconfigure
/**
 * Auto-configuration for AI Retry
 * Provides beans for retry template and response error handling
 * Handles transient and non-transient exceptions based on HTTP status codes
 *
 * Activated when RetryUtils is on the classpath
 * Automatically imports SpringAiRetryProperties for configuration
 * 
 * Auto-configuration order: After DataSourceAutoConfiguration, Before WebMvcAutoConfiguration
 * Configuration properties prefix: "spring.ai.retry"
 */
@AutoConfiguration
@ConditionalOnClass(RetryUtils.class)
@EnableConfigurationProperties({ SpringAiRetryProperties.class })
public class SpringAiRetryAutoConfiguration {

    /**
     * Creates a RetryTemplate bean with exponential backoff
     * Bean name: "retryTemplate"
     * Only created if no RetryTemplate bean already exists
     *
     * Configured to:
     * - Retry on TransientAiException
     * - Retry on ResourceAccessException (Spring's I/O exception for network errors)
     * - Retry on WebClientRequestException (if WebFlux available, detected at runtime)
     * - Use exponential backoff with configurable parameters
     * - Log warnings on each retry attempt
     * 
     * Retry listener logs format:
     * "Retry error. Retry count: " + context.getRetryCount() + ", Exception: " + throwable.getMessage()
     * 
     * Maximum total time (worst case):
     * With defaults (max-attempts=10, initial=2s, multiplier=5, max=180s):
     * ~1802 seconds = ~30 minutes total for all retries
     * 
     * Backoff progression with defaults:
     * Attempt 0: no wait (initial call)
     * Attempt 1: 2s wait
     * Attempt 2: 10s wait (2 × 5)
     * Attempt 3: 50s wait (10 × 5)
     * Attempt 4: 180s wait (50 × 5, capped at max)
     * Attempts 5-9: 180s wait each (capped at max)
     *
     * @param properties Configuration properties for retry behavior
     * @return Configured RetryTemplate instance (never null)
     */
    @Bean
    @ConditionalOnMissingBean
    public RetryTemplate retryTemplate(SpringAiRetryProperties properties);

    /**
     * Creates a ResponseErrorHandler bean for HTTP error classification
     * Bean name: "responseErrorHandler"
     * Only created if no ResponseErrorHandler bean already exists
     *
     * Returns an implementation of ResponseErrorHandler with the following methods:
     * - boolean hasError(ClientHttpResponse response): Checks if response has error status
     * - void handleError(ClientHttpResponse response): Handles error based on status code
     * - void handleError(URI, HttpMethod, ClientHttpResponse): Handles error with context
     *
     * Classifies HTTP errors based on status codes and configuration (in precedence order):
     * 1. HTTP codes in onHttpCodes list → TransientAiException (retry)
     * 2. HTTP codes in excludeOnHttpCodes list → NonTransientAiException (no retry)
     * 3. 4xx when onClientErrors=false (default) → NonTransientAiException (no retry)
     * 4. 4xx when onClientErrors=true → TransientAiException (retry)
     * 5. All other errors (5xx, network, etc.) → TransientAiException (retry)
     *
     * Error message format: "HTTP {status_code} - {response_body}"
     * If response body is null/empty: "HTTP {status_code} - No response body available"
     * Response body is read as UTF-8 String, limited to 4KB to prevent memory issues
     *
     * @param properties Configuration properties for error handling behavior
     * @return Configured ResponseErrorHandler instance (never null)
     */
    @Bean
    @ConditionalOnMissingBean
    public ResponseErrorHandler responseErrorHandler(SpringAiRetryProperties properties);
}

RetryTemplate Bean

The auto-configured RetryTemplate provides the following behavior:

Retry Conditions

The template retries on the following exception types:

  1. TransientAiException: Explicitly indicates a transient error (from spring-ai-retry)

    • Package: org.springframework.ai.retry
    • Use: Thrown by ResponseErrorHandler for retryable HTTP errors
    • Example: Server errors (5xx), rate limits (429 when configured)
  2. ResourceAccessException: Network/connection errors from Spring's RestTemplate

    • Package: org.springframework.web.client
    • Use: Thrown by RestTemplate for I/O errors
    • Example: Connection timeout, connection refused, network unreachable
  3. WebClientRequestException: Network/connection errors from Spring WebFlux WebClient (if WebFlux is on classpath)

    • Package: org.springframework.web.reactive.function.client
    • Use: Thrown by WebClient for I/O errors
    • Example: Connection errors in reactive applications
    • Detection: Loaded via reflection, skipped if WebFlux not available

Exponential Backoff

Backoff behavior is configured from properties:

  • Initial interval: Time before first retry (default: 2 seconds)

    • Property: spring.ai.retry.backoff.initial-interval
    • Type: Duration
    • Format: milliseconds (2000ms) or duration string (2s)
  • Multiplier: Growth factor for each retry (default: 5)

    • Property: spring.ai.retry.backoff.multiplier
    • Type: int
    • Range: >= 1 (1 = fixed backoff, >1 = exponential backoff)
  • Max interval: Maximum wait time (default: 3 minutes)

    • Property: spring.ai.retry.backoff.max-interval
    • Type: Duration
    • Format: milliseconds (180000ms) or duration string (3m)

Formula: wait_time = min(initial_interval × multiplier^(retry_count - 1), max_interval)

Backoff Examples

Default settings (initial=2s, multiplier=5, max=180s):

Attempt 0: immediate (no wait)
Attempt 1: wait 2s      (2 × 5^0 = 2)
Attempt 2: wait 10s     (2 × 5^1 = 10)
Attempt 3: wait 50s     (2 × 5^2 = 50)
Attempt 4: wait 180s    (2 × 5^3 = 250, capped at 180)
Attempt 5: wait 180s    (capped)
...
Attempt 9: wait 180s    (capped)
Total time: ~1802s = ~30 minutes

Conservative settings (initial=500ms, multiplier=2, max=5s):

Attempt 0: immediate
Attempt 1: wait 500ms   (0.5 × 2^0 = 0.5)
Attempt 2: wait 1s      (0.5 × 2^1 = 1)
Attempt 3: wait 2s      (0.5 × 2^2 = 2)
Attempt 4: wait 4s      (0.5 × 2^3 = 4)
Attempt 5: wait 5s      (0.5 × 2^4 = 8, capped at 5)
...
Total time for 10 attempts: ~42.5s

Fixed backoff (initial=1s, multiplier=1, max=1s):

All retries: wait 1s each
Total time for 10 attempts: ~9s

Retry Listener

The template includes a listener that logs warnings on each retry:

Log format:

Retry error. Retry count: {count}, Exception: {message}

Examples:

Retry error. Retry count: 1, Exception: HTTP 503 - Service temporarily unavailable
Retry error. Retry count: 2, Exception: HTTP 429 - Rate limit exceeded
Retry error. Retry count: 3, Exception: Connection timeout

The listener provides visibility into retry behavior during operation without requiring additional configuration.

Logger used: org.springframework.retry.support.RetryTemplate Log level: WARN

RetryTemplate API Methods

The configured RetryTemplate provides these key methods:

/**
 * Execute the callback with retry support
 * Retries on configured exceptions until max attempts reached
 * 
 * @param callback RetryCallback to execute
 * @param <T> Return type of callback
 * @param <E> Exception type that can be thrown
 * @return Result from successful callback execution
 * @throws E If all retries exhausted or non-retryable exception thrown
 */
<T, E extends Throwable> T execute(RetryCallback<T, E> callback) throws E;

/**
 * Execute the callback with retry and recovery support
 * If all retries fail, calls recovery callback instead of throwing exception
 * 
 * @param retryCallback RetryCallback to execute
 * @param recoveryCallback RecoveryCallback for fallback behavior
 * @param <T> Return type
 * @param <E> Exception type
 * @return Result from retry callback or recovery callback
 * @throws E If exception occurs during recovery
 */
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, 
                                    RecoveryCallback<T> recoveryCallback) throws E;

WebFlux Detection

The auto-configuration attempts to load WebClientRequestException at runtime:

// In retryTemplate() method
try {
    Class<?> webClientRequestEx = Class.forName(
        "org.springframework.web.reactive.function.client.WebClientRequestException"
    );
    // If class found, add to retryable exceptions
    retryTemplateBuilder.retryOn(webClientRequestEx);
} catch (ClassNotFoundException ignore) {
    // WebFlux not on classpath; skip WebClient support
    // No error or warning logged - this is expected behavior
}

This allows the same auto-configuration to work with or without WebFlux on the classpath.

Result:

  • With WebFlux: Retries on TransientAiException, ResourceAccessException, WebClientRequestException
  • Without WebFlux: Retries on TransientAiException, ResourceAccessException

ResponseErrorHandler Bean

The auto-configured ResponseErrorHandler classifies HTTP errors for retry decisions.

ResponseErrorHandler Interface

The bean returns an anonymous implementation of the ResponseErrorHandler interface with the following methods:

/**
 * Checks if the HTTP response has an error status code
 * Implementation: return response.getStatusCode().isError();
 * 
 * @param response The client HTTP response to check
 * @return true if status code is 4xx or 5xx (error), false for 2xx/3xx
 * @throws IOException if an I/O error occurs reading the status
 */
boolean hasError(ClientHttpResponse response) throws IOException;

/**
 * Handles the error in the HTTP response
 * Reads response body and throws appropriate exception based on status code
 * 
 * Response body handling:
 * - Read as UTF-8 String
 * - Limited to 4KB to prevent memory issues with large error responses
 * - If empty/null, uses placeholder: "No response body available"
 * 
 * Error message format: "HTTP {status_code} - {response_body}"
 * Examples:
 * - "HTTP 429 - Rate limit exceeded. Retry after 60 seconds."
 * - "HTTP 401 - Invalid API key provided."
 * - "HTTP 503 - No response body available"
 * 
 * @param response The client HTTP response with error status
 * @throws IOException if an I/O error occurs reading the response body
 * @throws TransientAiException for retryable errors (5xx, configured codes)
 * @throws NonTransientAiException for non-retryable errors (4xx, excluded codes)
 */
void handleError(ClientHttpResponse response) throws IOException;

/**
 * Handles the error with additional request context
 * Delegates to handleError(ClientHttpResponse) after optional logging
 * 
 * @param url The URL that was requested
 * @param method The HTTP method used (GET, POST, PUT, DELETE, etc.)
 * @param response The client HTTP response with error status
 * @throws IOException if an I/O error occurs
 * @throws TransientAiException for retryable errors
 * @throws NonTransientAiException for non-retryable errors
 */
void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException;

Error Classification Logic

The handler follows this decision flow (in precedence order):

1. Check if response has error status code (4xx or 5xx)
   ↓ No → Return (no error)
   ↓ Yes
2. Extract error message from response body (UTF-8, max 4KB)
   ↓
3. Is status code in properties.getOnHttpCodes()?
   ↓ Yes → Throw TransientAiException (retry) ✓
   ↓ No
4. Is status code in properties.getExcludeOnHttpCodes()?
   ↓ Yes → Throw NonTransientAiException (no retry) ✗
   ↓ No
5. Is status code 4xx (400-499)?
   ↓ Yes
   ├─ Is properties.isOnClientErrors() true?
   │  ↓ Yes → Throw TransientAiException (retry) ✓
   │  ↓ No → Throw NonTransientAiException (no retry) ✗
   ↓ No (5xx or other)
6. Default → Throw TransientAiException (retry) ✓

Error Message Format

Error messages include the HTTP status code and response body:

Format: HTTP {status_code} - {response_body}

Examples:

HTTP 429 - Rate limit exceeded. Please retry after 60 seconds.
HTTP 401 - Invalid authentication credentials.
HTTP 503 - Service temporarily unavailable due to maintenance.
HTTP 500 - Internal server error: NullPointerException at line 42
HTTP 502 - Bad Gateway
HTTP 504 - Gateway Timeout

If the response body is empty or null:

HTTP 503 - No response body available
HTTP 500 - No response body available

Response body reading:

  • Encoding: UTF-8
  • Size limit: 4KB (4096 bytes)
  • Behavior if larger: Truncated to 4KB
  • Behavior if empty: Uses placeholder text

HasError Implementation

The hasError method checks if the response status code is an error:

// Implementation in responseErrorHandler bean
public boolean hasError(ClientHttpResponse response) throws IOException {
    HttpStatusCode statusCode = response.getStatusCode();
    // isError() returns true for 4xx and 5xx codes
    return statusCode.isError();
}

Returns true for:

  • All 4xx status codes (400-499)
  • All 5xx status codes (500-599)

Returns false for:

  • 2xx status codes (200-299) - Success
  • 3xx status codes (300-399) - Redirection

HandleError Implementation

The handleError method reads the response body and throws the appropriate exception:

// Pseudocode for handleError implementation
public void handleError(ClientHttpResponse response) throws IOException {
    int statusCode = response.getStatusCode().value();
    
    // Read response body (UTF-8, max 4KB)
    String responseBody = readResponseBody(response);
    if (responseBody == null || responseBody.isEmpty()) {
        responseBody = "No response body available";
    }
    
    String errorMessage = "HTTP " + statusCode + " - " + responseBody;
    
    // Classification logic
    if (properties.getOnHttpCodes().contains(statusCode)) {
        throw new TransientAiException(errorMessage);
    }
    
    if (properties.getExcludeOnHttpCodes().contains(statusCode)) {
        throw new NonTransientAiException(errorMessage);
    }
    
    if (statusCode >= 400 && statusCode < 500) {
        if (properties.isOnClientErrors()) {
            throw new TransientAiException(errorMessage);
        } else {
            throw new NonTransientAiException(errorMessage);
        }
    }
    
    // Default: all other errors are transient (primarily 5xx)
    throw new TransientAiException(errorMessage);
}

Error Classification Examples

Example configuration:

spring.ai.retry.on-client-errors=false
spring.ai.retry.on-http-codes=429,503
spring.ai.retry.exclude-on-http-codes=401,403,400

Classification results:

  • 200 OK: No error, hasError() returns false
  • 301 Moved: No error, hasError() returns false
  • 400 Bad Request: NonTransientAiException (in excludeOnHttpCodes)
  • 401 Unauthorized: NonTransientAiException (in excludeOnHttpCodes)
  • 403 Forbidden: NonTransientAiException (in excludeOnHttpCodes)
  • 404 Not Found: NonTransientAiException (4xx and onClientErrors=false)
  • 422 Unprocessable: NonTransientAiException (4xx and onClientErrors=false)
  • 429 Rate Limit: TransientAiException (in onHttpCodes, highest precedence)
  • 500 Internal: TransientAiException (default for 5xx)
  • 502 Bad Gateway: TransientAiException (default for 5xx)
  • 503 Unavailable: TransientAiException (in onHttpCodes)
  • 504 Timeout: TransientAiException (default for 5xx)

Usage Examples

Basic Usage with Auto-Configuration

Simply add the dependency and the beans are auto-configured:

import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.stereotype.Service;

@Service
public class AiClientService {

    private final RetryTemplate retryTemplate;
    private final ResponseErrorHandler errorHandler;

    // Constructor injection - Spring auto-wires the auto-configured beans
    public AiClientService(
            RetryTemplate retryTemplate,
            ResponseErrorHandler errorHandler) {
        this.retryTemplate = retryTemplate;
        this.errorHandler = errorHandler;
    }

    public String callAiApi() {
        return retryTemplate.execute(context -> {
            // context is RetryContext with methods:
            // - getRetryCount(): int (0 for first attempt)
            // - getLastThrowable(): Throwable (null on first attempt)
            // - getAttribute(name): Object (custom attributes)
            return performApiCall();
        });
    }

    private String performApiCall() {
        // Implementation that may throw TransientAiException
        return "result";
    }
}

Using with RestTemplate

Configure RestTemplate with the auto-configured error handler:

import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RestTemplateConfig {

    /**
     * Creates RestTemplate with auto-configured error handler
     * Error handler will:
     * 1. Be invoked for all 4xx and 5xx responses
     * 2. Throw TransientAiException or NonTransientAiException
     * 3. Allow RetryTemplate to handle retry logic
     */
    @Bean
    public RestTemplate aiRestTemplate(ResponseErrorHandler errorHandler) {
        RestTemplate restTemplate = new RestTemplate();
        // Set error handler - invoked before response returned to caller
        restTemplate.setErrorHandler(errorHandler);
        return restTemplate;
    }
}

Combining RetryTemplate with RestTemplate

Use both beans together for comprehensive retry handling:

import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestTemplate;
import org.springframework.stereotype.Service;

@Service
public class AiService {

    private final RetryTemplate retryTemplate;
    private final RestTemplate restTemplate;

    public AiService(RetryTemplate retryTemplate, RestTemplate aiRestTemplate) {
        this.retryTemplate = retryTemplate;
        this.restTemplate = aiRestTemplate;  // Has auto-configured error handler
    }

    public String getAiCompletion(String prompt) {
        return retryTemplate.execute(context -> {
            // Flow:
            // 1. RestTemplate makes HTTP request
            // 2. If response is 4xx/5xx, ResponseErrorHandler is invoked
            // 3. ResponseErrorHandler throws TransientAiException or NonTransientAiException
            // 4. RetryTemplate catches TransientAiException and retries
            // 5. RetryTemplate propagates NonTransientAiException immediately
            return restTemplate.postForObject(
                "https://api.example.com/complete",
                prompt,
                String.class
            );
        });
    }
}

WebClient Support (WebFlux)

When Spring WebFlux is on the classpath, the RetryTemplate automatically handles WebClient exceptions:

import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
public class ReactiveAiService {

    private final RetryTemplate retryTemplate;
    private final WebClient webClient;

    public ReactiveAiService(RetryTemplate retryTemplate, WebClient.Builder builder) {
        this.retryTemplate = retryTemplate;
        this.webClient = builder.baseUrl("https://api.example.com").build();
    }

    /**
     * Uses RetryTemplate with reactive WebClient
     * Note: block() makes the call synchronous, compatible with RetryTemplate
     * For fully reactive retry, use reactor-retry instead
     */
    public String getAiCompletion(String prompt) {
        return retryTemplate.execute(context -> {
            // WebClientRequestException thrown on network errors
            // RetryTemplate catches and retries (when WebFlux detected)
            return webClient.post()
                .uri("/complete")
                .bodyValue(prompt)
                .retrieve()
                .bodyToMono(String.class)
                .block();  // Synchronous blocking call
        });
    }
}

Using RetryContext

Access retry context for advanced scenarios:

import org.springframework.retry.support.RetryTemplate;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;

public class AdvancedRetryService {

    private final RetryTemplate retryTemplate;

    public String callWithContext() {
        return retryTemplate.execute(new RetryCallback<String, RuntimeException>() {
            @Override
            public String doWithRetry(RetryContext context) throws RuntimeException {
                // Get retry count (0-based)
                int retryCount = context.getRetryCount();
                
                // Get last exception (null on first attempt)
                Throwable lastException = context.getLastThrowable();
                
                // Set/get custom attributes
                context.setAttribute("customKey", "customValue");
                Object value = context.getAttribute("customKey");
                
                // Log retry attempt
                System.out.println("Attempt " + (retryCount + 1));
                
                return performApiCall();
            }
        });
    }

    private String performApiCall() {
        return "result";
    }
}

Using RecoveryCallback

Provide fallback behavior when all retries fail:

import org.springframework.retry.support.RetryTemplate;
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryContext;

public class RetryWithFallbackService {

    private final RetryTemplate retryTemplate;

    public String callWithFallback() {
        return retryTemplate.execute(
            // Retry callback
            context -> performApiCall(),
            // Recovery callback - invoked when all retries exhausted
            new RecoveryCallback<String>() {
                @Override
                public String recover(RetryContext context) throws Exception {
                    // context.getRetryCount() = max attempts
                    // context.getLastThrowable() = last exception thrown
                    
                    Throwable lastError = context.getLastThrowable();
                    System.err.println("All retries failed: " + lastError.getMessage());
                    
                    // Return fallback value instead of throwing exception
                    return "Fallback response - service temporarily unavailable";
                }
            }
        );
    }

    private String performApiCall() {
        return "result";
    }
}

Customization and Override

Override RetryTemplate

Provide your own RetryTemplate bean to override the auto-configuration:

import org.springframework.retry.support.RetryTemplate;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CustomRetryConfig {

    /**
     * Custom RetryTemplate bean
     * Overrides auto-configured bean due to @ConditionalOnMissingBean
     */
    @Bean
    public RetryTemplate retryTemplate() {
        // Using builder API
        return RetryTemplate.builder()
            .maxAttempts(5)
            .fixedBackoff(1000)  // 1 second fixed backoff
            .retryOn(Exception.class)  // Retry on any exception
            .build();
    }
    
    /**
     * Alternative: Manual configuration with policies
     */
    @Bean
    public RetryTemplate retryTemplateManual() {
        RetryTemplate template = new RetryTemplate();
        
        // Retry policy
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(3);
        template.setRetryPolicy(retryPolicy);
        
        // Backoff policy
        FixedBackOffPolicy backoffPolicy = new FixedBackOffPolicy();
        backoffPolicy.setBackOffPeriod(2000);  // 2 seconds
        template.setBackOffPolicy(backoffPolicy);
        
        return template;
    }
}

The auto-configuration will detect this bean and skip creating its own due to @ConditionalOnMissingBean.

Override ResponseErrorHandler

Provide your own ResponseErrorHandler bean:

import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.HttpStatusCode;
import org.springframework.ai.retry.TransientAiException;
import org.springframework.ai.retry.NonTransientAiException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;

@Configuration
public class CustomErrorHandlerConfig {

    /**
     * Custom ResponseErrorHandler bean
     * Overrides auto-configured bean due to @ConditionalOnMissingBean
     */
    @Bean
    public ResponseErrorHandler responseErrorHandler() {
        return new ResponseErrorHandler() {
            
            @Override
            public boolean hasError(ClientHttpResponse response) throws IOException {
                HttpStatusCode statusCode = response.getStatusCode();
                // Custom logic: treat 404 as non-error
                if (statusCode.value() == 404) {
                    return false;
                }
                return statusCode.isError();
            }

            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                int statusCode = response.getStatusCode().value();
                String body = readBody(response);
                String message = "HTTP " + statusCode + " - " + body;
                
                // Custom classification logic
                switch (statusCode) {
                    case 429:  // Rate limit
                    case 503:  // Service unavailable
                        throw new TransientAiException(message);
                    case 401:  // Unauthorized
                    case 403:  // Forbidden
                        throw new NonTransientAiException(message);
                    default:
                        if (statusCode >= 500) {
                            throw new TransientAiException(message);
                        } else {
                            throw new NonTransientAiException(message);
                        }
                }
            }
            
            private String readBody(ClientHttpResponse response) throws IOException {
                // Read response body
                return "error details";
            }
        };
    }
}

Extend Auto-Configuration

Add additional configuration alongside the auto-configuration:

import org.springframework.retry.RetryListener;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryCallback;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Counter;

@Configuration
public class AdditionalRetryConfig {

    /**
     * Custom RetryListener for metrics/logging
     * Note: This doesn't override the auto-configured RetryTemplate
     * To use this listener, manually add it to RetryTemplate or create custom bean
     */
    @Bean
    public RetryListener customRetryListener(MeterRegistry meterRegistry) {
        Counter retryCounter = meterRegistry.counter("ai.retry.attempts");
        Counter failureCounter = meterRegistry.counter("ai.retry.failures");
        
        return new RetryListener() {
            
            @Override
            public <T, E extends Throwable> boolean open(
                    RetryContext context, 
                    RetryCallback<T, E> callback) {
                // Called before first attempt
                return true;  // true = proceed with retry
            }
            
            @Override
            public <T, E extends Throwable> void close(
                    RetryContext context,
                    RetryCallback<T, E> callback,
                    Throwable throwable) {
                // Called after all attempts complete
                if (throwable != null) {
                    failureCounter.increment();
                }
            }
            
            @Override
            public <T, E extends Throwable> void onError(
                    RetryContext context,
                    RetryCallback<T, E> callback,
                    Throwable throwable) {
                // Called after each failed attempt
                retryCounter.increment();
                System.out.println("Retry attempt " + (context.getRetryCount() + 1) + 
                                   " failed: " + throwable.getMessage());
            }
        };
    }
}

Conditional Behavior

ConditionalOnClass

The auto-configuration only activates when RetryUtils.class is on the classpath:

@ConditionalOnClass(RetryUtils.class)

This happens automatically when the spring-ai-retry dependency is included:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-retry</artifactId>
</dependency>

If spring-ai-retry is not on the classpath, the entire auto-configuration is skipped.

ConditionalOnMissingBean

Both bean factory methods use @ConditionalOnMissingBean:

@Bean
@ConditionalOnMissingBean
public RetryTemplate retryTemplate(SpringAiRetryProperties properties);

@Bean
@ConditionalOnMissingBean
public ResponseErrorHandler responseErrorHandler(SpringAiRetryProperties properties);

Behavior:

  • If no RetryTemplate bean exists: auto-configuration creates one
  • If a RetryTemplate bean already exists: auto-configuration skips creation
  • Same logic applies to ResponseErrorHandler

This allows custom beans to override auto-configured beans.

WebFlux Detection

The auto-configuration attempts to load WebClientRequestException at runtime:

// Inside retryTemplate() method
try {
    Class<?> webClientRequestEx = Class.forName(
        "org.springframework.web.reactive.function.client.WebClientRequestException"
    );
    // If class found, add it to retryable exceptions
    retryTemplateBuilder.retryOn(webClientRequestEx);
} catch (ClassNotFoundException ignore) {
    // WebFlux not on classpath; skip
    // No error or warning - this is expected behavior
}

This allows the same auto-configuration to work with or without WebFlux:

With WebFlux dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Result: RetryTemplate retries on TransientAiException, ResourceAccessException, WebClientRequestException

Without WebFlux: Result: RetryTemplate retries on TransientAiException, ResourceAccessException

Integration with Spring AI

This auto-configuration module is designed to integrate seamlessly with Spring AI components:

  1. AI Model Clients: Spring AI's model client beans can inject the auto-configured RetryTemplate

    • Example: OpenAI client, Anthropic client, etc.
    • Inject via constructor: public OpenAiClient(RetryTemplate retryTemplate)
  2. RestTemplate/WebClient: AI clients that use these HTTP clients benefit from the ResponseErrorHandler

    • RestTemplate: Set error handler via setErrorHandler()
    • WebClient: RetryTemplate handles WebClientRequestException
  3. Error Classification: TransientAiException and NonTransientAiException provide semantic error types for AI operations

    • Transient: Network errors, rate limits, server errors
    • Non-transient: Auth errors, bad requests, configuration errors
  4. Configuration: All retry behavior is configurable through standard Spring Boot properties

    • Prefix: spring.ai.retry
    • IDE autocomplete support via configuration metadata
    • Environment-specific profiles (dev, prod)

The auto-configuration ensures that retry behavior is consistent across all AI operations in a Spring AI application.

Boot Auto-Configuration Registration

The auto-configuration is registered via Spring Boot's auto-configuration mechanism:

File location: META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

File content:

org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration

Spring Boot automatically discovers and applies this auto-configuration when:

  1. The jar is on the classpath
  2. The file exists in META-INF/spring/
  3. The conditions are met (@ConditionalOnClass, @ConditionalOnMissingBean)

Discovery process:

  1. Spring Boot scans all jars for AutoConfiguration.imports files
  2. Loads auto-configuration classes listed in the files
  3. Evaluates conditional annotations (@ConditionalOnClass, etc.)
  4. Creates beans if conditions are satisfied

Configuration Properties Integration

The auto-configuration is tightly integrated with SpringAiRetryProperties via the @EnableConfigurationProperties annotation:

@EnableConfigurationProperties({ SpringAiRetryProperties.class })

This ensures that:

  1. Properties are bound from application.properties or application.yml

    • Prefix: spring.ai.retry
    • Binding happens before bean creation
    • Type-safe conversion (Duration, int, List<Integer>)
  2. Properties are validated at startup

    • Non-null constraints
    • Valid ranges (maxAttempts >= 0, multiplier >= 1)
    • Type compatibility
  3. Properties are injected into the bean factory methods

    • Via method parameter: retryTemplate(SpringAiRetryProperties properties)
    • Properties fully initialized and validated
  4. Changes to properties (in dev mode) trigger bean recreation

    • Spring Boot DevTools support
    • Live reload of retry configuration

See Configuration Properties for detailed property documentation.

tessl i tessl/maven-org-springframework-ai--spring-ai-autoconfigure-retry@1.1.1

docs

index.md

tile.json