spring mvc rest service redirect / forward / proxy

JavaSpring MvcSpring BootResttemplate

Java Problem Overview


I have build a web application using spring mvc framework to publish REST services. For example:

@Controller
@RequestMapping("/movie")
public class MovieController {

@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public @ResponseBody Movie getMovie(@PathVariable String id, @RequestBody user) {
	
	return dataProvider.getMovieById(user,id);

}
 

Now I need to deploy my application but I have the following problem: The clients do not have direct access to the computer on which the application resides (There is a firewall). Therefore I need a redirection layer on a proxy machine (accessible by the clients) which calls the actual rest service.

I tried making a new call using RestTemplate: For Example:

@Controller
@RequestMapping("/movieProxy")
public class MovieProxyController {

    private String address= "http://xxx.xxx.xxx.xxx:xx/MyApp";

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public @ResponseBody Movie getMovie(@PathVariable String id,@RequestBody user,final HttpServletResponse response,final HttpServletRequest request) {
	
	    HttpHeaders headers = new HttpHeaders();
	    headers.setContentType(MediaType.APPLICATION_JSON);
	    RestTemplate restTemplate = new RestTemplate();
	    return restTemplate.exchange( address+ request.getPathInfo(), request.getMethod(), new HttpEntity<T>(user, headers), Movie.class);

}

This is ok but I need to rewrite each method in the controller to use the resttemplate. Also, this causes redundant serialization/deserialization on the proxy machine.

I tried writing a generic function using restemplate, but it did not work out:

@Controller
@RequestMapping("/movieProxy")
public class MovieProxyController {

    private String address= "http://xxx.xxx.xxx.xxx:xx/MyApp";

    @RequestMapping(value = "/**")
    public ? redirect(final HttpServletResponse response,final HttpServletRequest request) { 		
	    HttpHeaders headers = new HttpHeaders();
	    headers.setContentType(MediaType.APPLICATION_JSON);
	    RestTemplate restTemplate = new RestTemplate();
	    return restTemplate.exchange( address+ request.getPathInfo(), request.getMethod(), ? , ?);

}

I could not find a method of resttemplate which works with request and response objects.

I also tried spring redirect and forward. But redirect does not change the request's client ip address so i think it is useless in this case. I could not forward to another URL either.

Is there a more appropriate way to achieve this? Thanks in advance.

Java Solutions


Solution 1 - Java

You can mirror/proxy all requests with this:

private String server = "localhost";
private int port = 8080;

@RequestMapping("/**")
@ResponseBody
public String mirrorRest(@RequestBody String body, HttpMethod method, HttpServletRequest request) throws URISyntaxException
{
    URI uri = new URI("http", null, server, port, request.getRequestURI(), request.getQueryString(), null);

    ResponseEntity<String> responseEntity =
        restTemplate.exchange(uri, method, new HttpEntity<String>(body), String.class);

    return responseEntity.getBody();
}

This will not mirror any headers.

Solution 2 - Java

Here's my modified version of the original answer, which differs in four points:

  1. It does not make the request body mandatory, and as such does not let GET requests fail.
  2. It copies all headers present in the original request. If you are using another proxy/web server, this can cause issues due to content length/gzip compression. Limit the headers to the ones you really need.
  3. It does not reencode the query params or the path. We expect them to be encoded anyway. Note that other parts of your URL might also be encoded. If that is the case for you, leverage the full potential of UriComponentsBuilder.
  4. It does return error codes from the server properly.

@RequestMapping("/**")
public ResponseEntity mirrorRest(@RequestBody(required = false) String body, 
    HttpMethod method, HttpServletRequest request, HttpServletResponse response) 
    throws URISyntaxException {
    String requestUrl = request.getRequestURI();

    URI uri = new URI("http", null, server, port, null, null, null);
    uri = UriComponentsBuilder.fromUri(uri)
                              .path(requestUrl)
                              .query(request.getQueryString())
                              .build(true).toUri();

    HttpHeaders headers = new HttpHeaders();
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) {
        String headerName = headerNames.nextElement();
        headers.set(headerName, request.getHeader(headerName));
    }

    HttpEntity<String> httpEntity = new HttpEntity<>(body, headers);
    RestTemplate restTemplate = new RestTemplate();
    try {
        return restTemplate.exchange(uri, method, httpEntity, String.class);
    } catch(HttpStatusCodeException e) {
        return ResponseEntity.status(e.getRawStatusCode())
                             .headers(e.getResponseHeaders())
                             .body(e.getResponseBodyAsString());
    }
}

Solution 3 - Java

You can use Netflix Zuul to route requests coming to a spring application to another spring application.

Let's say you have two application: 1.songs-app, 2.api-gateway

In the api-gateway application, first add the zuul dependecy, then you can simply define your routing rule in application.yml as follows:

pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    <version>LATEST</version>
</dependency>

application.yml

server:
  port: 8080
zuul:
  routes:
    foos:
      path: /api/songs/**
      url: http://localhost:8081/songs/

and lastly run the api-gateway application like:

@EnableZuulProxy
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Now, the gateway will route all the /api/songs/ requests to http://localhost:8081/songs/.

A working example is here: https://github.com/muatik/spring-playground/tree/master/spring-api-gateway

Another resource: http://www.baeldung.com/spring-rest-with-zuul-proxy

Solution 4 - Java

proxy controller with oauth2

@RequestMapping("v9")
@RestController
@EnableConfigurationProperties
public class ProxyRestController {
    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails;

    @Autowired
    private ClientCredentialsResourceDetails clientCredentialsResourceDetails;

    @Autowired
    OAuth2RestTemplate oAuth2RestTemplate;


    @Value("${gateway.url:http://gateway/}")
    String gatewayUrl;

    @RequestMapping(value = "/proxy/**")
    public String proxy(@RequestBody(required = false) String body, HttpMethod method, HttpServletRequest request, HttpServletResponse response,
                        @RequestHeader HttpHeaders headers) throws ServletException, IOException, URISyntaxException {

        body = body == null ? "" : body;
        String path = request.getRequestURI();
        String query = request.getQueryString();
        path = path.replaceAll(".*/v9/proxy", "");
        StringBuffer urlBuilder = new StringBuffer(gatewayUrl);
        if (path != null) {
            urlBuilder.append(path);
        }
        if (query != null) {
            urlBuilder.append('?');
            urlBuilder.append(query);
        }
        URI url = new URI(urlBuilder.toString());
        if (logger.isInfoEnabled()) {
            logger.info("url: {} ", url);
            logger.info("method: {} ", method);
            logger.info("body: {} ", body);
            logger.info("headers: {} ", headers);
        }
        ResponseEntity<String> responseEntity
                = oAuth2RestTemplate.exchange(url, method, new HttpEntity<String>(body, headers), String.class);
        return responseEntity.getBody();
    }


    @Bean
    @ConfigurationProperties("security.oauth2.client")
    @ConditionalOnMissingBean(ClientCredentialsResourceDetails.class)
    public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
        return new ClientCredentialsResourceDetails();
    }

    @Bean
    @ConditionalOnMissingBean
    public OAuth2RestTemplate oAuth2RestTemplate() {
        return new OAuth2RestTemplate(clientCredentialsResourceDetails);
    }


Solution 5 - Java

@derkoe has posted a great answer that helped me a lot!

Trying this in 2021, I was able to improve on it a little:

  1. You don't need @ResponseBody if your class is a @RestController
  2. @RequestBody(required = false) allows for requests without a body (e.g. GET)
  3. https and port 443 for those ssl encrypted endpoints (if your server serves https on port 443)
  4. If you return the entire responseEntity instead of only the body, you also get the headers and response code.
  5. Example of added (optional) headers, e.g. headers.put("Authorization", Arrays.asList(String[] { "Bearer 234asdf234"})
  6. Exception handling (catches and forwards HttpStatuses like 404 instead of throwing a 500 Server Error)

private String server = "localhost";
private int port = 443;

@Autowired
MultiValueMap<String, String> headers;

@Autowired
RestTemplate restTemplate;

@RequestMapping("/**")
public ResponseEntity<String> mirrorRest(@RequestBody(required = false) String body, HttpMethod method, HttpServletRequest request) throws URISyntaxException
{
    URI uri = new URI("https", null, server, port, request.getRequestURI(), request.getQueryString(), null);

    HttpEntity<String> entity = new HttpEntity<>(body, headers);    
    
    try {
		ResponseEntity<String> responseEntity =
			restTemplate.exchange(uri, method, entity, String.class);
			return responseEntity;
	} catch (HttpClientErrorException ex) {
		return ResponseEntity
			.status(ex.getStatusCode())
			.headers(ex.getResponseHeaders())
			.body(ex.getResponseBodyAsString());
	}

    return responseEntity;
}

Solution 6 - Java

If you can get away with using a lower-level solution like mod_proxy that would be the simpler way to go, but if you need more control (e.g. security, translation, business logic) you may want to take a look at Apache Camel: http://camel.apache.org/how-to-use-camel-as-a-http-proxy-between-a-client-and-server.html

Solution 7 - Java

I got inspired by Veluria's solution, but I had issues with gzip compression sent from the target resource.

The goal was to omit Accept-Encoding header:

@RequestMapping("/**")
public ResponseEntity mirrorRest(@RequestBody(required = false) String body, 
    HttpMethod method, HttpServletRequest request, HttpServletResponse response) 
    throws URISyntaxException {
    String requestUrl = request.getRequestURI();

    URI uri = new URI("http", null, server, port, null, null, null);
    uri = UriComponentsBuilder.fromUri(uri)
                              .path(requestUrl)
                              .query(request.getQueryString())
                              .build(true).toUri();

    HttpHeaders headers = new HttpHeaders();
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) {
        String headerName = headerNames.nextElement();
        if (!headerName.equals("Accept-Encoding")) {
            headers.set(headerName, request.getHeader(headerName));
        }
    }

    HttpEntity<String> httpEntity = new HttpEntity<>(body, headers);
    RestTemplate restTemplate = new RestTemplate();
    try {
        return restTemplate.exchange(uri, method, httpEntity, String.class);
    } catch(HttpStatusCodeException e) {
        return ResponseEntity.status(e.getRawStatusCode())
                             .headers(e.getResponseHeaders())
                             .body(e.getResponseBodyAsString());
    }
}

Solution 8 - Java

You need something like jetty transparent proxy, which actually will redirect your call, and you get a chance to overwrite the request if you needed. You may get its detail at http://reanimatter.com/2016/01/25/embedded-jetty-as-http-proxy/

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionnilgunView Question on Stackoverflow
Solution 1 - JavaderkoeView Answer on Stackoverflow
Solution 2 - JavaSteffenView Answer on Stackoverflow
Solution 3 - JavaMuatikView Answer on Stackoverflow
Solution 4 - Javalove aduView Answer on Stackoverflow
Solution 5 - JavaJonas EicherView Answer on Stackoverflow
Solution 6 - JavaChris H.View Answer on Stackoverflow
Solution 7 - Javamichal.jakubeczyView Answer on Stackoverflow
Solution 8 - Javakrmanish007View Answer on Stackoverflow