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

Have Routes Support Multiple URIs #114

Open
ryanjbaxter opened this issue Nov 16, 2017 · 37 comments
Open

Have Routes Support Multiple URIs #114

ryanjbaxter opened this issue Nov 16, 2017 · 37 comments

Comments

@ryanjbaxter
Copy link
Contributor

It would be nice if a route could be configured to make requests to multiple URIs and then return the results to a specified controller to aggregate the responses.

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
      # =====================================
      - id: greeting
        uris: 
        - lb://greeting
        - lb://name
        predicates:
        - Path=/greeting/**
        controller: blah

The controller, blah in this case, would take the result of calling the greeting and name services and return a response for the client.

For example if the greeting service returned Hello and the name service returned Ryan. The controller would return Hello Ryan.

@spencergibb
Copy link
Member

It would have to return something structured. Like

{
  "greeting": "Hello",
  "name": "Ryan"
}

Something automated. Maybe some kind of non-blocking template

{{greeting.response}} {{name.response}}

@ryanjbaxter
Copy link
Contributor Author

ryanjbaxter commented Apr 3, 2018

@spencergibb I have been thinking more about this recently...wondering what you thought of this

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
      - id: greeting
        uri: http://greeting
        predicates:
        .....
        filters:
        ......
      - id: name
        uri: lb://name
        filters:
        ......
      - id: my-composite
         routes:
         - id: greeting
           path: /foo
           headers:
             cookie: foo,bar
             host: www.greeting.com
           method: post
           userequestbody: true
         - id: name
           path: /bar
           headers:
             cookie: foo,bar
             host: www.name.com
           method: get
         predicates: 
         ....
         filters:
         ....

Some import things of note

  • Predicates: I was thinking removing the requirement for a route to have predicates (which I think is a requirement today based on what I see in Route.Builder.build). That way I can define a route that might not be accessible itself but still be part of a composite route.

  • Filters: I am a little unsure of how the filters will work in a composite route. In some cases like hystrix or rate limiter, I could see if applying to the entire composite route. However something like add header doesn't seem to make sense. I could see it making sense if it added that header to each route in the composite but then that seems to have a different behavior from hystrix or rate limiting and I am not sure if that makes sense or not.

@michael-barker
Copy link

I'd love to have this feature for BFFs. We're currently investigating using Gateway for BFF endpoints that can be directly proxied downstream but when composing APIs we still have to create controllers and manually handle requests to have parity with all the things Gateway does.

@spencergibb
Copy link
Member

I think we could get there. A couple of issues to consider: transforming individual responses (which we're working on) and combining the multiple requests. I was thinking of maybe using something like timeleaf(sp?) That supports reactive templates.

@ryanjbaxter
Copy link
Contributor Author

I have been working on prototyping this, stay tuned for more updates as I play around with things.

@fitzoh
Copy link
Contributor

fitzoh commented May 5, 2018

So wouldn't the nested routes under a composite route make binding a little trickier and yaml structure a bit more complicated?

I've been poking around with a different approach that would use the existing YAML structure and do the work in filters:
https://github.com/Fitzoh/spring-cloud-gateway-114-spike

There's a ForkingGatewayFilterFactory and a JoiningGatewayFilterFactory.

The forking filter makes a request to Config.uri, and stores the response in an exchange attribute map (Config.name -> Mono<ClientResponse>)

The joining filter grabs the response monos out of the exchange attribute, transforms the responses from Map<String, Mono<ClientResponse>> -> Mono<Map<String, ClientResponse>> and writes it to the response.

Sample java config

r.path("/forkjoin")
        .filters(f -> {
            f.filter(forkingGatewayFilterFactory.apply(c -> c.setName("fork1").setUri("http://httpbin.org/anything/fork1")));
            f.filter(forkingGatewayFilterFactory.apply(c -> c.setName("fork2").setUri("http://httpbin.org/anything/fork2")));
            f.filter(joiningGatewayFilterFactory.apply(c -> c.setName("join")));
            return f;
        })
        .uri("http://google.com"))

super rough output :
$ curl http://localhost:8080/forkjoin

{fork2={
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip", 
    "Connection": "close", 
    "Host": "httpbin.org", 
    "User-Agent": "ReactorNetty/0.7.6.RELEASE"
  }, 
  "json": null, 
  "method": "GET", 
  "origin": "67.149.185.133", 
  "url": "http://httpbin.org/anything/fork2"
}
, fork1={
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip", 
    "Connection": "close", 
    "Host": "httpbin.org", 
    "User-Agent": "ReactorNetty/0.7.6.RELEASE"
  }, 
  "json": null, 
  "method": "GET", 
  "origin": "67.149.185.133", 
  "url": "http://httpbin.org/anything/fork1"
}
}

I'm not sure if the JoiningFilter is the right way to go or not, could also potentially do a forward instead.

@ryanjbaxter
Copy link
Contributor Author

@fitzoh what if in the filters i want to apply things like filters to modify the request/response for each route individually? The is why I liked referencing existing route definitions. I wanted to run each route of the "composite" through the existing RoutePredicateHandlerMapper.

@fitzoh
Copy link
Contributor

fitzoh commented Jul 7, 2018

That makes sense @ryanjbaxter, but the predicates feel a little weird to me in that approach.

Do we just ignore the top level predicates for composite routes? Can there be additional predicates within a composite route that do get honored? (ie maybe we never want it to be available as a top level route, and only sometimes get called as a composite route)

On an overall feature level I feel like this has the potential to add a lot of cognitive overhead, and config file support would leave you w/ too much yaml to make sense of (at least for me), especially for figuring out how to reassemble downstream requests in a way that makes sense.

To me this seems like something that should be done in code, potentially w/ some utility functions to make it a bit less awkward.

@spencergibb
Copy link
Member

Dave added the ProxyExchange obejct for webflux in the spring-cloud-gateway-webflux which would make this easy in code. Could use a simple request mapping or forward routes

@alxsimo
Copy link

alxsimo commented Jul 10, 2018

What's the status of this? Think it will be a really cool feature.

@spencergibb
Copy link
Member

No change in status

@dave-fl
Copy link

dave-fl commented Aug 9, 2018

I feel like this might be similarly related.

If I want to do a conditional flow e.g.

uri targeta = "uria"
uri targetb = "urib"
bool a = true; // might not always be true
if (a) then goto targeta else goto targetb

What would be the proper way to express something like this?

@spencergibb
Copy link
Member

@dave-fl predicates are booleans, so two different routes with appropriate predicates (you can use not() in the javadsl or a custom predicate)

@dave-fl
Copy link

dave-fl commented Aug 9, 2018

Thank you @spencergibb

Let me add some more details.

The input to the predicate will only be known at runtime - can I have a custom predicate to determine this?

targeta and targetb must be discovered at runtime (they are not fixed and must be sourced from another service) - how can I set the URI dynamically?

Additionally if I visit targetb and the response is successful, I would like to revisit targeta.

@spencergibb
Copy link
Member

That seems beyond the scope of this issue. Any filter can set the uri. Something custom is your only solution ATM.

@sskurapati
Copy link

Is there any status update on this?

@ryanjbaxter
Copy link
Contributor Author

Nothing yet

@ninad050
Copy link

ninad050 commented Feb 6, 2019

@ryanjbaxter any updates on when can we expect this feature ?

@spencergibb
Copy link
Member

There are currently no plans right now

@spencergibb
Copy link
Member

Not sure why it got added to a project

@natraj09
Copy link

Would love to have this feature as well. Anyway we can upvote this issue to help prioritize?

@spencergibb
Copy link
Member

@natraj09 add your thumbs up here #114 (comment)

This would be a major feature and we already have a programmatic way to do this with ProxyExchange. I'm not sure how much we can improve on that anyway. Without being able to customize how multiple responses are joined I don't see much value in doing this.

@sincang
Copy link

sincang commented Mar 29, 2019

Hi,

Sorry I may missed but is multiple URIs already supported. I am thinking of using this feature for legacy systems.

I can do work around by registering in eureka in behalf of the legacy apps. But it would be nice if gateway supports this.

@ryanjbaxter
Copy link
Contributor Author

@sincang no it is not supported yet

@patpatpat123
Copy link

Hello Spring Cloud Gateway Team,

This would be a great feature!
Hopefully, we can see this available.
Thank you

@sthanaraju
Copy link

sthanaraju commented Apr 7, 2019

Hi SCG Team,

I am building an api gateway using SCG. There are some cases where we need to make 3 sequence of requests to different URIs for a single request. The output of 1st request is the input / path param / headers for subsequent request. And we expect the response are aggregated sometimes, and sometimes we need the 2nd request response.

If SCG supports this feature, it would have been a best feature in it.

Thanks.

@dave-fl
Copy link

dave-fl commented Apr 8, 2019

@sthanaraju What you are describing and what is being asked from here seem to be two different things.

The output of 1st request is the input / path param / headers for subsequent request"

If you need to do this, just use WebClient, if you want to be more low level, you can use HttpClient.

@spencergibb
Copy link
Member

You could also use ProxyExchange from spring-cloud-gateway-webflux.

@dave-fl
Copy link

dave-fl commented Apr 8, 2019

@spencergibb I noticed that in the documentation @RestController is used.

If it is possible, this should really leverage functional endpoints as they seem to operate at an order of magnitude faster relative to their annotated counterparts.

@spencergibb
Copy link
Member

The webflux version is new https://cloud.spring.io/spring-cloud-gateway/2.1.x/multi/multi__building_a_simple_gateway_using_spring_mvc_or_webflux.html Gateway has no functional endpoint functionality yet.

@emreavsar
Copy link

Is there any news on this? We tried to create a custom filter, which can do a rest call via webclient, but the big problem i see is that i need to pass in a uri for proxying the request to. In this case I would rather have a terminal filter (or so) which resolves all mono's and returns the response body to the browser.

I've read that there is a programmatic way, can you give an example on this?

@pgsysemsinc
Copy link

Hi ,

I have a requirement, that is i will receive XML request and i need to split into small and send these xml's to down the service, also vice-versa once receive multiple responses from the server , i need to club into one and send back this to client.

Is this possible in spring-cloud gateway ?

@pgsysemsinc
Copy link

ForkingGatewayFilterFactory has issue not clearing prvous post requests, any idea ?

@lacima
Copy link

lacima commented Aug 30, 2021

Came across this thread because I am doing some Axway/Vordel policy ports and was trying SCG on for size.

The requirement is to be able to call an authentication endpoint first prior to sending on to the actual endpoint.

How about something along the lines of:

spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: auth
uri: http://some-internal-auth
next: processor
predicates:
.....
filters:
......
- id: processor
uri: http://some-internal-processor
predicates:
.....
filters:
......

So {next:} only forwards to another route when there are no errors...

@rozagerardo
Copy link

This feature would indeed be quite useful.
For the moment, I resolved it using the ModifyResponseBodyGatewayFilterFactory in my own Filter.

Just in case it's useful for anyone else, I provide the base implementation here (it may need some rework, but it should be enough to make the point).

Simply put, I have a "base" service retrieving something like this:

[
  {
    "targetEntryId": "624a448cbc728123b47d08c4",
    "sections": [
      {
        "title": "sadasa",
        "description": "asda"
      }
    ],
    "id": "624a448c45459c4d757869f1"
  },
  {
    "targetEntryId": "624a44e5bc728123b47d08c5",
    "sections": [
      {
        "title": "asda",
        "description": null
      }
    ],
    "id": "624a44e645459c4d757869f2"
  }
]

And I want to enrich these entries with the actual targetEntry data (of course, identified by targetEntryId).

So, I created my Filter based on the ModifyResponseBody one:

/**
 * <p>
 *   Filter to compose a response body with associated data from a second API.
 * </p>
 *
 * @author rozagerardo
 */
@Component
public class ComposeFieldApiGatewayFilterFactory extends
    AbstractGatewayFilterFactory<ComposeFieldApiGatewayFilterFactory.Config> {

  public ComposeFieldApiGatewayFilterFactory() {
    super(Config.class);
  }

  @Autowired
  ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilter;

  ParameterizedTypeReference<List<Map<String, Object>>> jsonType =
      new ParameterizedTypeReference<List<Map<String, Object>>>() {
      };

  @Value("${server.port:9080}")
  int aPort;

  @Override
  public GatewayFilter apply(final Config config) {
    return modifyResponseBodyFilter.apply((c) -> {
      c.setRewriteFunction(List.class, List.class, (filterExchange, input) -> {
        List<Map<String, Object>> castedInput = (List<Map<String, Object>>) input;
        //  extract base field values (usually ids) and join them in a "," separated string
        String baseFieldValues = castedInput.stream()
            .map(bodyMap -> (String) bodyMap.get(config.getOriginBaseField()))
            .collect(Collectors.joining(","));

        // Request to a path managed by the Gateway
        WebClient client = WebClient.create();
        return client.get()
            .uri(UriComponentsBuilder.fromUriString("http://localhost").port(aPort)
                .path(config.getTargetGatewayPath())
                .queryParam(config.getTargetQueryParam(), baseFieldValues).build().toUri())
            .exchangeToMono(response -> response.bodyToMono(jsonType)
                .map(targetEntries -> {
                  // create a Map using the base field values as keys fo easy access
                  Map<String, Map> targetEntriesMap = targetEntries.stream().collect(
                      Collectors.toMap(pr -> (String) pr.get("id"), pr -> pr));
                  // compose the origin body using the requested target entries
                  return castedInput.stream().map(originEntries -> {
                    originEntries.put(config.getComposeField(),
                        targetEntriesMap.get(originEntries.get(config.getOriginBaseField())));
                    return originEntries;
                  }).collect(Collectors.toList());
                })
            );
      });
    });
  }

  ;

  @Override
  public List<String> shortcutFieldOrder() {
    return Arrays.asList("originBaseField", "targetGatewayPath", "targetQueryParam",
        "composeField");
  }

  /**
   * <p>
   * Config class to use for AbstractGatewayFilterFactory.
   * </p>
   */
  public static class Config {

    private String originBaseField;
    private String targetGatewayPath;
    private String targetQueryParam;
    private String composeField;

    public Config() {
    }

    // Getters and Setters...

  }
}

For completeness, this is the corresponding route setup using my Filter:

spring:
  cloud:
    gateway:
      routes:
        # TARGET ENTRIES ROUTES
        - id: targetentries_route
          uri: ${configs.api.tagetentries.baseURL}
          predicates:
            - Path=/api/target/entries
            - Method=GET
          filters:
            - RewritePath=/api/target/entries(?<segment>.*), /target-entries-service$\{segment}
        # ORIGIN ENTRIES
        - id: originentries_route
          uri: ${configs.api.originentries.baseURL}
          predicates:
            - Path=/api/origin/entries**
          filters:
            - RewritePath=/api/origin/entries(?<segment>.*), /origin-entries-service$\{segment}
            - ComposeFieldApi=targetEntryId,/api/target/entries,ids,targetEntry

And with this, my resulting response looks as follows:

[
  {
    "targetEntryId": "624a448cbc728123b47d08c4",
    "sections": [
      {
        "title": "sadasa",
        "description": "asda"
      }
    ],
    "id": "624a448c45459c4d757869f1",
    "targetEntry": {
      "id": "624a448cbc728123b47d08c4",
      "targetEntityField": "whatever"
    }
  },
  {
    "targetEntryId": "624a44e5bc728123b47d08c5",
    "sections": [
      {
        "title": "asda",
        "description": null
      }
    ],
    "id": "624a44e645459c4d757869f2",
    "targetEntry": {
      "id": "624a44e5bc728123b47d08c5",
      "targetEntityField": "somethingelse"
    }
  }
]

@ghoshtir
Copy link

ghoshtir commented Sep 16, 2022

I currently have a requirement on similar lines.

1 request to the gateway needs to be routed to 2 different service providing uris (preferable parallelly) and somehow both the responses need to be sent back to the requestor. Is this possible with the current features of spring boot gateway.

@y-luis-rojo
Copy link

Interested in such feature as well. Any update?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests