I have a few use cases in my project, that require me to call the same endpoint on several different base URLs and I need to be able to pass the base URL as a parameter into the method of the client.

Using Feign, this is possible to achieve by just adding a URI as a parameter. In a spring http interface, there is also the possibility to add a URI as a parameter, but the functionality is not the same because here it overwrites the whole path that is set in the HttpExchange annotation. What I want is to provide only the base URL as a parameter but that the path in the annotation gets added to the base URL.

For example,

If i do

    @GetExchange(value = "/animals/cats/", accept = APPLICATION_JSON_VALUE)
    List<Cat> getCats(URI baseUrl)

and provide https://some.cats.url.com as value for the baseUrl parameter

it will send a request to the URL https://some.url.com but actually what I want to do is to call https://some.url.com/animals/cats.

Thanks!

Comment From: OlgaMaciaszek

Hi @mprevisic this, in fact, works in Feign, however it would be a breaking change for us and it might not be a feature that is very commonly used. Seeing that you can already override the entire URI, most scenarios should be handled. For your specific-use case, if there are not too many different base urls you use, you might want to create various clients using the same interface, but with WebClients, each with a specific baseUrl set. Alternatively, you might also handle this use-case by passing the url as a request attribute and creating a client filter/interceptor to further handle it.

Comment From: floriandreher

Hi @OlgaMaciaszek thanks for your reply. I understand, that this would be a breaking change and maybe not that any people would need a feature like that, but maybe a few people will need it :) and it would be maybe encourage people to migrate from feign to http interfaces, if clients behave the same. Maybe i'm missing something, but i can't see a usage for the current implementation. Why would someone define a path and do not want to use it?

If you don't like the idea of a breaking change, would it be possible, that a path variable like @PathVariable URI baseUrl would be encoded? So that a user could do something like:

@GetExchange(value = "{baseUrl}/animals/cats/", accept = APPLICATION_JSON_VALUE)
List<Cat> getCats(@PathVariable URI baseUrl)

At the moment this would call something like http%3A%2F%2F/animals/cats/

Comment From: rstoyanchev

Maybe i'm missing something, but i can't see a usage for the current implementation.

That's not a good enough reason to break compatibility. It would be a regression to someone else whose use case is different from yours.

This is not to say that we don't want to make your use case easy. But given where we are today, we'll need to imagine a way in the programming model to make the intent clear.

I'm afraid the suggestion for @PathVariable won't work as it's meant to apply to the path.

Note also that in our clients (HTTP and WebSocket), you can usually provide a URI template that we expand that into a URI or you provide an already prepared URI and we use it as is. That's worth keeping in mind as something many are accustomed to from using Spring HTTP clients.

I'm now wondering about supporting this through a UriComponentsBuilder argument. While URI implies a fully prepared and encoded URL, a UriComponentsBuilder implies a partially prepared URL to which we can add the path from the annotation and then build it.

Comment From: rstoyanchev

Better yet, we can support UriBuilderFactory as an argument, similar to what we support on RestTemplate, and now also on the new RestClient. You would create a DefaultUriBuilderFactory with the base URL (scheme, host, port, or even base path if you want). Pass that into the HTTP service interface method as UriBuilderFactory, and internally we call UriBuilderFactory#expand with the URI template from the annotation to build the complete URL.

Comment From: floriandreher

Thanks for your reply @rstoyanchev

That's not a good enough reason to break compatibility. It would be a regression to someone else whose use case is different from yours.

Such a break is not nice. However, i think, that most people will migrate from feign to http-interfaces and so they are used to feign behavior.

But your suggestion with the UriBuilderFactory would be fine for me. Can we set the path then on the interface like:

@GetExchange(value = "/animals/{type}", accept = APPLICATION_JSON_VALUE)
List<Animal> getAnimals(UriBuilderFactory factory, @PathVariable(value = "type") String type)

var factory = new DefaultUriBuilderFactory(UriComponentsBuilder.fromUri(URI.create("http://example.com")));
getAnimals(factory, "cat");

or do we have to do something like:

@GetExchange(accept = APPLICATION_JSON_VALUE)
List<Animal> getAnimals(UriBuilderFactory factory, @PathVariable(value = "type") String type)

var factory = new DefaultUriBuilderFactory(UriComponentsBuilder.fromUriString("http://example.com/animals/{type}")));
getAnimals(factory, "cat");

Comment From: rstoyanchev

You would be able to do:

@GetExchange(value = "/animals/{type}", accept = APPLICATION_JSON_VALUE)
List<Animal> getAnimals(UriBuilderFactory factory, @PathVariable(value = "type") String type)

var uriBuilderfactory = new DefaultUriBuilderFactory("http://example.com");
getAnimals(uriBuilderfactory, "cat");

Internally we would call:

var uriTemplate = annotation.url();
var uri = uriBuilderFactory.expand(uriTemplate);

which would expand the URI template from the annotation with the base URL in the UriBuilderFactory. In effect, replacing the internal UriBuilderFactory with the base URL that each WebClient and RestTemplate can be configured with.

Comment From: rstoyanchev

Superseded by #31413.

Comment From: yuexueyang

Althougth use UriBuilderFactory as a parameter can solve the issue to determine the base url at runtime, but it will need some extra unnessesary code in business logic, in feign, this work can be done on a request interceptor, by setting the target. Dose this product can support this feature?

Besides, as I see from document, every declared interface need to be created at a configuration class one by one, but in OpenFeign there's no need to do so. Dose this product can do the same thing as OpenFeign

Comment From: OlgaMaciaszek

@yuexueyang, we provide this method of doing it here. It is rather straightforward. You could also opt for getting your base url for the underlying client from properties.

Regarding auto-configuration, I'm working on it now. Track progress under https://github.com/spring-projects/spring-boot/issues/31337.

Comment From: rezamirali-leanix

@rstoyanchev or @OlgaMaciaszek could you please provide a more detailed example? In the sample code you have writing calling a method inside the interface which has the GetExchange which is not possible theoretically.

Comment From: bclozel

@rezamirali-leanix here is a complete example:

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.util.UriBuilderFactory;

public interface ApiServiceClient {

    @GetExchange("/{method}")
    String makeApiRequest(UriBuilderFactory uriBuilderFactory, @PathVariable String method);
}
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriBuilderFactory;

@SpringBootApplication
public class IntclientApplication {

    public static void main(String[] args) {
        SpringApplication.run(IntclientApplication.class, args);
    }

    @Bean
    public ApplicationRunner runCommand(RestClient.Builder builder) {
        return new ApplicationRunner() {
            @Override
            public void run(ApplicationArguments args) throws Exception {
                RestClient restClient = builder.build();
                RestClientAdapter adapter = RestClientAdapter.create(restClient);
                HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

                ApiServiceClient service = factory.createClient(ApiServiceClient.class);

                var uriBuilderfactory = new DefaultUriBuilderFactory("http://httpbin.org/");
                String result = service.makeApiRequest(uriBuilderfactory, "get");

                System.out.println(result);
            }
        };
    }
}