title: Spring Cloud Gateway - composite API calls
body: <p>I have a YAML-configured API Gateway that is hiding a large amount of services. In the near future, we will need a way to create composite API calls, such as the one described in <a href="https://github.com/spring-cloud/spring-cloud-gateway/issues/114" rel="nofollow noreferrer">this</a> thread. Seems like it's not yet available in Spring Cloud Gateway, as it is an open issue. I can see that Spencer Gibb has referred to the usage of <code>ProxyExchange</code>, but I'm a bit unclear how I can use that in a gateway filter, as it seems more suited towards a controller instead.</p>
  <p>So here is my setup:
  I have two exposed endpoints running at <code>localhost:9000</code>. Here's the controller class:</p>
  <pre class="lang-java prettyprint-override"><code>@RestController
  @RequestMapping(&quot;/composite&quot;)
  public class CompositeCallController {
  
      @GetMapping(&quot;/test/one&quot;)
      public Map&lt;String, Object&gt; first() {
          return Map.of(&quot;response-1&quot;, &quot;FIRST&quot;);
  
      }
  
      @GetMapping(&quot;/test/two&quot;)
      public Map&lt;String, Object&gt; second() {
          return Map.of(&quot;response-2&quot;, &quot;SECOND&quot;);
      }
  }
  </code></pre>
  <p>My gateway is running on <code>localhost:8080</code>. I have a route that hits the first service from the above controller - <code>/composite/test/one</code>. My idea is to have a route filter that uses <code>ModifyResponseBodyGatewayFilterFactory</code> and modifies the body to include the response that comes from calling the second endpoint - <code>/composite/test/two</code>. The expected result is something like this:</p>
  <pre class="lang-json prettyprint-override"><code>{
      &quot;1&quot;: {
          &quot;response-1&quot;: &quot;FIRST&quot;
      },
      &quot;2&quot;: {
          &quot;response-2&quot;: &quot;SECOND&quot;
      }
  }
  </code></pre>
  <p>To capture the second response, I have made a DTO:</p>
  <pre class="lang-java prettyprint-override"><code>@Getter
  @Setter
  public class ApiCallDTO {
      private Map&lt;String, Object&gt; response;
  }
  </code></pre>
  <p>Here's the filter that I try to do all of this in:</p>
  <pre class="lang-java prettyprint-override"><code>@Component
  public class CompositeApiCallFilter extends AbstractGatewayFilterFactory&lt;CompositeApiCallFilter.Config&gt; {
      public static final String AG_HOST = &quot;8080&quot;;
      public static final String SERVICES_HOST = &quot;9000&quot;;
      private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
  
      @Autowired
      public CompositeApiCallFilter(ModifyResponseBodyGatewayFilterFactory factory) {
          super(Config.class);
          this.modifyResponseBodyFilterFactory = factory;
      }
  
      @Override
      public GatewayFilter apply(final Config config) {
          final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
          modifyResponseBodyFilterFactoryConfig.setRewriteFunction(Map.class, Map.class, (exchange, firstCallBody) -&gt; {
              ServerHttpRequest request = exchange.getRequest();
              String serviceURL = getServiceURL(request);
              WebClient client = WebClient.create();
  
              Mono&lt;ApiCallDTO&gt; apiCallDTOMono = client.get()
                      .uri(uriBuilder -&gt; uriBuilder.path(serviceURL + &quot;/composite/test/two&quot;).build())
                      .retrieve()
                      .bodyToMono(ApiCallDTO.class);
  
              Map&lt;String, Object&gt; output = new HashMap&lt;&gt;();
              apiCallDTOMono
                      .doOnNext(r -&gt; output.put(&quot;1&quot;, firstCallBody))
                      .subscribe(value -&gt; output.put(&quot;2&quot;, value.getResponse()));
  
              return Mono.just(output);
          });
          return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
      }
  
      public static class Config {
      }
  
      private String getServiceURL(ServerHttpRequest request) {
          return request.getURI().toString().replace(request.getPath().toString(), &quot;&quot;).replace(AG_HOST, SERVICES_HOST);
      }
  
  </code></pre>
  <p>And the respective YML config:</p>
  <pre class="lang-yml prettyprint-override"><code>spring:
    cloud:
      gateway:
        routes:
          - id: composite-call-test
            uri: http://localhost:9000
            predicates:
              - Path=/composite/**
            filters:
              - CompositeApiCallFilter
  </code></pre>
  <p>This is about as barebones as I need it for now, as it's just a POC task at this point. However, when I hit <code>localhost:8080/composite/test/one</code> through Postman, I get back an empty map with status code 200, which makes me think the actions in the <code>subscribe()</code> call are not being executed at all, and I don't understand why.</p>
  <p>Additionally, I see a bunch of errors show up in the console exactly 5 seconds after the request has been made:</p>
  <ul>
  <li><code>Caused by: org.springframework.web.reactive.function.client.WebClientRequestException: Failed to resolve 'http:' after 3 queries ; nested exception is java.net.UnknownHostException: Failed to resolve 'http:' after 3 queries</code></li>
  <li><code>Caused by: java.net.UnknownHostException: Failed to resolve 'http:' after 3 queries</code></li>
  <li><code>Caused by: io.netty.resolver.dns.DnsNameResolverTimeoutException: [/192.168.0.1:53] query via UDP timed out after 5000 milliseconds (no stack trace available)</code></li>
  </ul>
  <p>Additionally, I tried merging the two calls into a new Mono by using the <code>.zip()</code> method, and then transferring the Mono contents into a new map like so:</p>
  <pre class="lang-java prettyprint-override"><code>    public GatewayFilter apply(final Config config) {
          final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
          modifyResponseBodyFilterFactoryConfig.setRewriteFunction(Map.class, Map.class, (exchange, firstCall) -&gt; {
              ServerHttpRequest request = exchange.getRequest();
              String serviceURL = getServiceURL(request);
              WebClient client = WebClient.create();
  
              Mono&lt;Map&lt;String, Object&gt;&gt; firstCallDTOMono = Mono.just(firstCall);
  
              Mono&lt;SecondCallDTO&gt; secondCallDTOMono = client.get()
                      .uri(uriBuilder -&gt; uriBuilder.path(serviceURL + &quot;/composite/test/two&quot;).build())
                      .retrieve()
                      .bodyToMono(SecondCallDTO.class);
  
              Mono&lt;MergedCallDTO&gt; mergedCallDTOMono = Mono
                      .zip(firstCallDTOMono, secondCallDTOMono)
                      .map(mergedMono -&gt; {
                          MergedCallDTO mergedCallDTO = new MergedCallDTO();
                          mergedCallDTO.setResponse1(Map.of(&quot;response-1&quot;, mergedMono.getT1().get(&quot;response-1&quot;)));
                          mergedCallDTO.setResponse2(mergedMono.getT2().getResponse());
                          return mergedCallDTO;
                      });
  
              Map&lt;String, Object&gt; output = new HashMap&lt;&gt;();
  
              mergedCallDTOMono.map(response -&gt; {
                  output.put(&quot;1&quot;, response.getResponse1().get(&quot;response-1&quot;));
                  output.put(&quot;2&quot;, response.getResponse2().get(&quot;response-2&quot;));
                  return output;
              });
  
              return Mono.just(output);
          });
          return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
      }
  </code></pre>
  <p>With both <code>FirstCallDTO</code> and <code>SecondCallDTO</code> being identical with <code>ApiCallDTO</code> that I mentioned above, and <code>MergedCallDTO</code> looking like this:</p>
  <pre class="lang-java prettyprint-override"><code>@Getter
  @Setter
  public class MergedCallDTO {
      private Map&lt;String, Object&gt; response1;
      private Map&lt;String, Object&gt; response2;
  }
  </code></pre>
  <p>But this doesn't seem to work either, as I still get back an empty map with status code 200, but this time without seeing any errors or exceptions being thrown.</p>
  <p>I'm quite lost at this point, I've tried at least 5 different implementations, and none of them have worked. Could anyone please point out the flaw in my approach? Thanks!</p>