Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RestClient calls interceptors from RestTemplate twice #32038

Closed
tilm4nn opened this issue Jan 16, 2024 · 7 comments
Closed

RestClient calls interceptors from RestTemplate twice #32038

tilm4nn opened this issue Jan 16, 2024 · 7 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Milestone

Comments

@tilm4nn
Copy link

tilm4nn commented Jan 16, 2024

This has been observed using spring-boot 3.2.1 with spring-web 6.1.2

When using RestClient.create(RestTemplate) and providing a RestTemplate instance that holds any HttpClientRequestInerceptors, RestClient copies the interceptor list and creates an own InterceptingClientHttpRequestFactory with the copied list.

But it also uses the InterceptingClientHttpRequestFactory obtained from RestTemplate.getRequestFactory() containing the (same) interceptors from the RestTemplate to create the requests.

Now when creating a request, a request chain containing two different instances of InterceptingClientHttpRequest each with an equal interceptor list are created and thus each interceptor is called twice during request execution.

There is a workaround, when you do not need to reuse the RestTemplate:

RestClient restClient = RestClient.create(restTemplate);
restTemplate.setInterceptors(List.of());

Complete code to reproduce:

    @Test
    void restclient_should_calls_interceptors_from_resttemplate_only_once() {

        AtomicInteger calls = new AtomicInteger();

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(List.of((request, body, execution) -> {
            calls.incrementAndGet();
            return execution.execute(request, body);
        }));

        RestClient restClient = RestClient.create(restTemplate);

        // Workaround when you do not need to reuse RestTemplate
        // restTemplate.setInterceptors(List.of());

        try {
            restClient.get().uri("https://localhost:8080/foobar").retrieve();
        } catch (ResourceAccessException e) {
            // http-call fails but does not matter.
        }

        assertEquals(1, calls.get());
    }
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jan 16, 2024
@sbrannen sbrannen added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Jan 16, 2024
@sbrannen sbrannen changed the title RestClient calls Intercepters from RestTemplate twice RestClient calls interceptors from RestTemplate twice Jan 16, 2024
@sbrannen sbrannen added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jan 16, 2024
@sbrannen
Copy link
Member

sbrannen commented Jan 16, 2024

Hi @tilm4nn,

Congratulations on reporting your first issue for the Spring Framework! 👍

Thanks for the detailed explanation and the test that reproduces the behavior.

We'll look into it.

@sbrannen sbrannen added this to the 6.1.4 milestone Jan 16, 2024
@injae-kim
Copy link
Contributor

injae-kim commented Jan 16, 2024

	@Test
	void restclient_should_calls_interceptors_from_resttemplate_only_once() {
		AtomicInteger calls = new AtomicInteger();

		RestTemplate restTemplate = new RestTemplate();
		restTemplate.setInterceptors(List.of((request, body, execution) -> {
			calls.incrementAndGet();
			return execution.execute(request, body);
		}));

		RestClient restClient = RestClient.create(restTemplate);
		
		try {
			restClient.get().uri("https://localhost:8080/foobar").retrieve();
		} catch (ResourceAccessException e) { }

		Assertions.assertEquals(1, calls.get()); // ❌ assertion failed, expected: <1> but was: <2>
               // It means, interceptors are called twice
	}

image

Thanks @tilm4nn for the detailed explanation! your reproduces works well with me too.

Root cause: RestTemplate.interceptors are used both on RestClient and RestTemplate twice.

factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);

  1. RestTemplate.getRequestFactory() return InterceptingClientHttpRequestFactory(interceptors). it contains interceptors

this.requestFactory = restTemplate.getRequestFactory();
this.messageConverters = new ArrayList<>(restTemplate.getMessageConverters());
if (!CollectionUtils.isEmpty(restTemplate.getInterceptors())) {
this.interceptors = new ArrayList<>(restTemplate.getInterceptors());
}

  1. But on RestClientBuilder(RestTemplate), we copy interceptors again that already contained in InterceptingClientHttpRequestFactory from restTemplate.getRequestFactory()

factory = new InterceptingClientHttpRequestFactory(DefaultRestClient.this.clientRequestFactory, DefaultRestClient.this.interceptors);

  1. Finally, RestClient.createRequest() uses interceptors and InterceptingClientHttpRequestFactory from restTemplate.getRequestFactory() that already contains interceptors, so same interceptors from RestTemplate called twice!

My simple proposal

if (!CollectionUtils.isEmpty(restTemplate.getInterceptors())) {
this.interceptors = new ArrayList<>(restTemplate.getInterceptors());
}

if (!CollectionUtils.isEmpty(restTemplate.getInterceptors())) {
        // 👇👇 If RestTemplate's requestFactory already contains interceptors, don't copy it 
	if (!(this.requestFactory instanceof InterceptingClientHttpRequestFactory)) {
		this.interceptors = new ArrayList<>(restTemplate.getInterceptors());
	}
}

With above code, we don't copy RestTemplate's interceptors when restTemplate.requestFactory already contains that interceptors.

And I checked test also passed with above code.

So simply check my investigation and share your opinion~! I'm also ready to open PR. thanks a lot! cc. @sbrannen 🙇

@tilm4nn
Copy link
Author

tilm4nn commented Jan 16, 2024

Hi @injae-kim, thank you very much for your investigation and proposal. I think the idea of the proposed solution is clear and it might be achieved in a simpler way. I can imagine only two possible paths:

  1. RestTemplate does hold some interceptors: then requestFactory will always be an InterceptingClientHttpRequestFactory here and we do not copy the interceptors list. (Only probably extremly rare exception: A subclass of RestTemplate is used that overrides getRequestFactory somehow)
  2. RestTemplate does not hold any interceptors: then we do not copy the interceptors list.

So as the outcome is always "we do not copy the interceptors list" wouldn't it be possible to just skip the copy of the interceptors list alltogether? Or am I missing something here?

One additional case comes to my mind:

var builder = RestClient.builder(restTemplateWithInterceptors);
builder.requestFactory(someSpecialRequestFactory)
// Here we have lost the interceptors of RestTemplate's InterceptingClientHttpRequestFactory and have to add them manually
builder.requestInterceptors(interceptors -> interceptors.addAll(restTemplateWithInterceptors.getInterceptors()))

But I think one can argue that the manual step is justified in this case.

@tilm4nn
Copy link
Author

tilm4nn commented Jan 16, 2024

And here is another workaround that also works independent of how the RestTemplate is used otherwise:

RestClient.builder(restTemplate).requestInterceptors(List::clear).build();

@injae-kim
Copy link
Contributor

injae-kim commented Jan 17, 2024

So as the outcome is always "we do not copy the interceptors list"
wouldn't it be possible to "just skip the copy of the interceptors list" all together?

Hi @tilm4nn ! as you suggested above, we can fix this issue by "just skip the copy of the interceptors list from RestTemplate".

RestTemplate does hold some interceptors: then requestFactory will always be an InterceptingClientHttpRequestFactory here and we do not copy the interceptors list. (Only probably extremly rare exception: A subclass of RestTemplate is used that overrides getRequestFactory somehow)

But I worry about this case. in the future I think we can change RestTemplate.requestFactory()'s return type that doesn't include interceptors, and then RestTemplate.interceptors are never copied!
(For maintainability, maybe it's better to not strongly rely on assumption that RestTemplate.requestFactory() always contains interceptors)

So let's wait maintainer's opinion about this~ thanks! 😃

@poutsma poutsma self-assigned this Jan 17, 2024
@poutsma
Copy link
Contributor

poutsma commented Jan 19, 2024

@tilm4nn Thanks for reporting this, it should now be fixed.

@injae-kim Changing what RestTemplate.getRequestFactory() returns might have solved this particular issue, but it would also break backward compatibility for existing users that rely on the current behavior, especially considering the fact that this method comes from InterceptingHttpAccessor, an abstract base class that could be in use in various other places, not just RestTemplate. So I decided to take a different route in c820e44.

@injae-kim
Copy link
Contributor

c820e44 Unwrap request factory when creating RestClient from RestTemplate

aha~ I understood. thanks for the kind explanation! I agree with your fix. it looks more explicit way! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Projects
None yet
Development

No branches or pull requests

5 participants