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

Enable dynamically overriding the base URL in HTTP interface #30935

Closed
mprevisic opened this issue Jul 24, 2023 · 11 comments
Closed

Enable dynamically overriding the base URL in HTTP interface #30935

mprevisic opened this issue Jul 24, 2023 · 11 comments
Assignees
Labels
status: superseded An issue that has been superseded by another type: enhancement A general enhancement

Comments

@mprevisic
Copy link

mprevisic commented Jul 24, 2023

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!

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jul 24, 2023
@OlgaMaciaszek
Copy link
Contributor

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.

@floriandreher
Copy link

floriandreher commented Sep 7, 2023

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/

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Sep 12, 2023

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.

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Sep 12, 2023

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.

@floriandreher
Copy link

floriandreher commented Sep 12, 2023

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");

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Sep 13, 2023

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.

@rstoyanchev rstoyanchev added this to the 6.1.0-RC1 milestone Sep 13, 2023
@rstoyanchev rstoyanchev added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Sep 13, 2023
@rstoyanchev rstoyanchev changed the title Enable dynamically setting base URL in http interface Enable dynamically overriding the base URL in HTTP interface Sep 13, 2023
@rstoyanchev rstoyanchev self-assigned this Oct 2, 2023
@rstoyanchev
Copy link
Contributor

Superseded by #31413.

@rstoyanchev rstoyanchev closed this as not planned Won't fix, can't repro, duplicate, stale Oct 11, 2023
@rstoyanchev rstoyanchev removed this from the 6.1.0-RC1 milestone Oct 11, 2023
@rstoyanchev rstoyanchev added the status: superseded An issue that has been superseded by another label Oct 11, 2023
@yuexueyang
Copy link

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

@OlgaMaciaszek
Copy link
Contributor

@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 spring-projects/spring-boot#31337.

@rezamirali-leanix
Copy link

@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.

@bclozel
Copy link
Member

bclozel commented Jan 3, 2025

@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);
			}
		};
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: superseded An issue that has been superseded by another type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants