Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/modules/ROOT/pages/spring-cloud-openfeign.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,14 @@ public interface DemoClient {

You can also disable the feature via property `spring.cloud.openfeign.cache.enabled=false`.

==== Limitations

Using `@Cacheable(sync = true)` with Feign clients may cause recursive cache invocation and result in an `IllegalStateException`.

This happens due to the interaction between Feign proxies and Spring Cache synchronization.

As a workaround, avoid using `sync = true` or disable Feign caching (`spring.cloud.openfeign.cache.enabled=false`).
Comment on lines +750 to +754
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new “Limitations” note about @Cacheable(sync = true) causing recursive invocation/IllegalStateException isn’t tied to the stated PR problem (duplicate cache error handler execution) and there’s no reference/link for the claim. Either link it to a tracked issue/reproducer or adjust the documentation to describe the actual limitation being addressed by this PR.

Suggested change
Using `@Cacheable(sync = true)` with Feign clients may cause recursive cache invocation and result in an `IllegalStateException`.
This happens due to the interaction between Feign proxies and Spring Cache synchronization.
As a workaround, avoid using `sync = true` or disable Feign caching (`spring.cloud.openfeign.cache.enabled=false`).
When using Spring's cache abstraction with Feign clients, cache error handlers may be invoked more than once for a single Feign call.
This behavior is due to the interaction between Feign proxies and Spring Cache infrastructure when handling cache failures.
As a workaround, design cache error handlers to be idempotent and avoid side effects, or disable Feign caching (`spring.cloud.openfeign.cache.enabled=false`) if this behavior is problematic.

Copilot uses AI. Check for mistakes.


[[spring-requestmapping-support]]
=== Spring @RequestMapping Support
Expand Down Expand Up @@ -879,6 +887,46 @@ protected interface DemoFeignClient {
}
----


=== FeignClientBuilder

`FeignClientBuilder` allows programmatic creation of Feign clients without using the `@FeignClient` annotation.

It builds clients in the same way as `@FeignClient`, but provides flexibility for dynamic use cases.

Unlike `@FeignClient`, which defines clients statically, `FeignClientBuilder` allows creating clients dynamically at runtime.

==== Basic Usage
Comment on lines +891 to +899
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added FeignClientBuilder section is unrelated to the caching bug described in the PR and significantly expands the scope of this change. If this is intentional, it should be split into a separate PR; otherwise please drop it from this one to keep the fix focused.

Copilot uses AI. Check for mistakes.

[source,java,indent=0]
----
@Autowired
private ApplicationContext applicationContext;

FeignClientBuilder builder = new FeignClientBuilder(applicationContext);

MyClient client = builder
.forType(MyClient.class, "myClient")
.url("http://localhost:8080")
.build();
----

==== Configuration Options

* `url(String url)` - Sets the target URL
* `path(String path)` - Adds a base path
* `contextId(String contextId)` - Unique identifier
* `dismiss404(boolean)` - Ignore 404 errors
* `inheritParentContext(boolean)` - Inherit parent config
* `fallback(Class<? extends T>)` - Fallback class
* `customize(FeignBuilderCustomizer)` - Customize Feign builder

==== When to Use

* Dynamic client creation
* Runtime configuration
* When `@FeignClient` is not sufficient

[[reactive-support]]
=== Reactive Support
As at the time of active development of Spring Cloud OpenFeign, the https://github.com/OpenFeign/feign[OpenFeign project] did not support reactive clients, such as https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/WebClient.html[Spring WebClient], such support could not be added to Spring Cloud OpenFeign either.
Expand All @@ -893,6 +941,7 @@ We discourage using Feign clients in the early stages of application lifecycle,
Similarly, depending on how you are using your Feign clients, you may see initialization errors when starting your application. To work around this problem you can use an `ObjectProvider` when autowiring your client.

[source,java,indent=0]

----
@Autowired
ObjectProvider<TestFeignClient> testFeignClient;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public class FeignCachingInvocationHandlerFactory implements InvocationHandlerFa

private final CacheInterceptor cacheInterceptor;

// ADDED: ThreadLocal guard
private static final ThreadLocal<Boolean> CACHE_IN_PROGRESS = ThreadLocal.withInitial(() -> false);

public FeignCachingInvocationHandlerFactory(InvocationHandlerFactory delegateFactory,
CacheInterceptor cacheInterceptor) {
this.delegateFactory = delegateFactory;
Expand All @@ -50,32 +53,45 @@ public InvocationHandler create(Target target, Map<Method, MethodHandler> dispat
final InvocationHandler delegateHandler = delegateFactory.create(target, dispatch);
return (proxy, method, argsNullable) -> {
Object[] args = Optional.ofNullable(argsNullable).orElseGet(() -> new Object[0]);
return cacheInterceptor.invoke(new MethodInvocation() {
@Override
public Method getMethod() {
return method;
}

@Override
public Object[] getArguments() {
return args;
}

@Override
public Object proceed() throws Throwable {
return delegateHandler.invoke(proxy, method, args);
}

@Override
public Object getThis() {
return target;
}

@Override
public AccessibleObject getStaticPart() {
return method;
}
});

// ✅ ADDED: Prevent nested cache invocation
if (CACHE_IN_PROGRESS.get()) {
Comment on lines 42 to +58
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the “ADDED” markers / emoji in comments. They aren’t used elsewhere in the codebase and will likely be distracting in long-lived source; replace with a neutral explanation of why the guard exists (ideally referencing the issue).

Copilot uses AI. Check for mistakes.
return delegateHandler.invoke(proxy, method, args);
}

try {
CACHE_IN_PROGRESS.set(true);

return cacheInterceptor.invoke(new MethodInvocation() {
Comment on lines +57 to +65
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ThreadLocal guard is only set inside this invocation handler, so it won’t be active when caching is initiated outside Feign (e.g., when a Spring AOP CacheInterceptor wraps the Feign proxy, as described in #681). In that flow this handler will still call cacheInterceptor.invoke(...), so the duplicate cache interception / error-handler execution would remain. Consider a mechanism that detects an already-active Spring Cache invocation (or prevents Feign caching from applying when Spring caching already proxies the client), and add a regression test that asserts cache get errors are handled only once.

Copilot uses AI. Check for mistakes.
@Override
public Method getMethod() {
return method;
}

@Override
public Object[] getArguments() {
return args;
}

@Override
public Object proceed() throws Throwable {
return delegateHandler.invoke(proxy, method, args);
}

@Override
public Object getThis() {
return target;
}

@Override
public AccessibleObject getStaticPart() {
return method;
}
});
}
finally {
CACHE_IN_PROGRESS.remove();
}
};
}

Expand Down
Loading