spring mvc rest service redirect / forward / proxy
Asked Answered
A

9

85

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?

Amazed answered 6/2, 2013 at 9:55 Comment(3)
Why couldn't you use something like Apache w/ mod_rewrite or mod_proxy to do this? You would basically put a webserver outside your firewall (usually we call this the DMZ), and setup rules in the FW that allows that server to talk to your server behind the firewall. This is how most companies solve this problem.Nunhood
thank you, i will try to talk to sys admins if your solution works for our case. Meanwhile i will be using resttemplate and serialize/deserialize json data to string..Amazed
Can anyone tell me how to do this for any post request ? What changes to be made to do this for a Post requestBufford
R
87

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.

Radiosensitive answered 19/5, 2014 at 11:25 Comment(12)
I was expecting an answer like yours but it has been long time and I am no longer working on that project. Unfortunately it not possible for me to make comprehensive tests and confirm your code right now.Amazed
What would the URL and parameters would look like to this controller? I have called the right URL but I am getting "the request sent by the client was syntactically incorrect" ?Rellia
My original Method signature looks like this (I didn't have RequestBody or HttpMethod at all ) @RequestMapping("/getProduct") public CustomMessage getCompanyResults(@RequestParam("search") String search, @RequestParam(value = "id", required = false) String rfgId, @RequestParam(value = "rowBegin", required = false) String )Rellia
This wouldn't work for multi-part requests, would it?Ainslee
I guess this won't work for multi-part requests! If you want to handle multi-part requests as well, you'd have to implement a full HTTP proxy.Radiosensitive
How this will mirror all the headers in request as you mentioned below?Doing
The original answer did mirror the headers - the current one doesntRadiosensitive
You can pass the headers. Just return not String, but ResponseEntity. You can create your own instance of ResponseEntity with some subset of responseEntity's headers or with some custom ones. Hope this will help someone.Versus
@koe I am unable to get this to work for multipart/form-data in practice.Stambaugh
@Stambaugh this implementation won't work for multipart since the files are not in the body. You'll have to implement special handling for that.Radiosensitive
If the REST API returns a 404 response or somesuch, will that get passed through as well? Or will the code just throw an exception?Doi
How would this look with WebFlux?Idiom
B
57

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());
    }
}
Backer answered 22/3, 2018 at 13:13 Comment(14)
I tried this code and its almost perfect. Except how can I return the real error code from the proxied server? Your code always returns 500 instead of the real status error code.Pattison
I've also modified my answer to include this behavior as I was actually missing out on it too.Backer
@Watt Does this also happen when you are not copying the headers? I mention this in my post specifically, since I ran into a similar issue, so in my case I just set content type to application/json and that's about it.Backer
@Veluria sorry, apparently I couldnt read properly that day. I did however run into issues with haproxy that was solved with some code from our old proxy servlet implementation headerName != null && !"Transfer-Encoding".equals(headerName);Watt
@Veluria If I understand this correctly, this code would not include multipartfiles from a form, would that be possible to add in a nice way?Watt
@Watt Since you need consumes="multipart/form-data" for this, I don't think it's possible to easily include this here, but if you succeed, let me know.Backer
@Veluria It's not the prettiest piece of code I've written but it works in our case pastebin.com/LApUmpxxWatt
Faced the same issue as the OP @Watt your code seems to work great with some minor adjustments. Ill let you know if I run into errors or have any issues. Thank you!Coppock
This works great, but for some reason I had to define a specific function for PUT. It just turns around and calls this function, so it's kinda weird. Worked fine if I disabled CORs in chrome, so I'm guessing it's something around that. I do have an unusual situation where it's browser <-> apache proxy <-> spring proxy <-> service.Sherasherar
Can this be used to mirror https request as well? I mean will there be anything to take care of ?Erythrite
@Erythrite Yes and not really. Just the headers.Backer
@Backer I am getting an error related to serialization here. org.codehaus.jackson.map.JsonMappingException: No serializer found for class org.springframework.http.ResponseEntity and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS)Erythrite
@Erythrite Well, did you try setting spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false in your application.properties?Backer
I couldn't get that to work so I had to map the ResponseEntity<String> into javax.response object and that seems to be working for me. But I'll try to do what you suggested as well.Erythrite
O
32

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

Oppenheimer answered 13/3, 2017 at 9:31 Comment(2)
Doesn't that kinda burdens the application with all the Cloud dependencies? Tried to use Zuul by itself with no effect. Got examples of using Zuul without Cloud?Cheekpiece
Zuul seems to be deprecated. Spring Cloud Gateway is the recommended way now.Meraz
I
8

@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;
}
Inanna answered 13/4, 2021 at 15:32 Comment(4)
looking back at this made me realize how much easier this is in node+express. Also less pitfalls with embedded servers (tomcat/undertow) that are trying to be helpful by adding/changing headers.Inanna
How is restTemplate and entity defined here?Jurist
HttpEntity<String> entity = new HttpEntity<>(body, headers);Inanna
restTemplate you can just let Spring Boot inject it via @Autowired (recommended) or manually new RestTemplate()Inanna
B
4

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


Br answered 22/1, 2019 at 7:22 Comment(0)
M
3

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

Mathew answered 3/1, 2014 at 23:9 Comment(0)
G
2

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());
    }
}
Goeselt answered 27/4, 2021 at 13:26 Comment(0)
G
0

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/

Gelderland answered 17/12, 2018 at 11:39 Comment(0)
M
0

RequestEntity, added in Spring Web 4.1, can be used as a controller method parameter to capture all the relevant HTTP state needed to make a REST call, and it can also be used by RestTemplate to make a REST call.

A REST call can be made using a copy of the controller method parameter's RequestEntity with an updated URL. The data can be retrieved as a byte[] and returned from the controller as a ResponseEntity<byte[]>. This is effectively a direct proxy without any in-app data conversion.

@Controller
@RequestMapping("/movieProxy")
public class MovieProxyController {
    private String address= "http://xxx.xxx.xxx.xxx:xx/MyApp";

    @RequestMapping(value = "/**")
    public ResponseEntity<byte[]> redirect(RequestEntity<byte[]> request) {
        URI uri = UriComponentsBuilder.fromUriString(address)
                .path(request.getUrl().getPath())
                .query(request.getUrl().getRawQuery()).build().toUri();

        RequestEntity<byte[]> requestCopy = new RequestEntity<>(
                request.getBody(), request.getHeaders(), request.getMethod(),
                uri, request.getType());
        try {
            return restTemplate.exchange(requestCopy, byte[].class);
        } catch (RestClientResponseException e) {
            return new ResponseEntity<>(e.getResponseBodyAsByteArray(),
                    e.getResponseHeaders(), e.getStatusCode());
        }
}
Multiplicate answered 5/8, 2023 at 5:13 Comment(1)
For some reason, RequestEntity seems to be missing the body in my case. With the same request, if I use a @RequestBody String argument, I get it instead.Thessa

© 2022 - 2024 — McMap. All rights reserved.