39

I got a HttpServletRequest request in my Spring Servlet which I would like to forward AS-IS (i.e. GET or POST content) to a different server.

What would be the best way to do it using Spring Framework?

Do I need to grab all the information and build a new HTTPUrlConnection? Or there is an easier way?

Mikhail Kholodkov
  • 23,642
  • 17
  • 61
  • 78
user1144031
  • 627
  • 2
  • 6
  • 16

6 Answers6

27

Discussions of whether you should do forwarding this way aside, here's how I did it:

package com.example.servlets;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Enumeration;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.servlets.GlobalConstants;

@SuppressWarnings("serial")
public class ForwardServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) {
        forwardRequest("GET", req, resp);
    }

    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse resp) {
        forwardRequest("POST", req, resp);
    }

    private void forwardRequest(String method, HttpServletRequest req, HttpServletResponse resp) {
        final boolean hasoutbody = (method.equals("POST"));

        try {
            final URL url = new URL(GlobalConstants.CLIENT_BACKEND_HTTPS  // no trailing slash
                    + req.getRequestURI()
                    + (req.getQueryString() != null ? "?" + req.getQueryString() : ""));
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod(method);

            final Enumeration<String> headers = req.getHeaderNames();
            while (headers.hasMoreElements()) {
                final String header = headers.nextElement();
                final Enumeration<String> values = req.getHeaders(header);
                while (values.hasMoreElements()) {
                    final String value = values.nextElement();
                    conn.addRequestProperty(header, value);
                }
            }

          //conn.setFollowRedirects(false);  // throws AccessDenied exception
            conn.setUseCaches(false);
            conn.setDoInput(true);
            conn.setDoOutput(hasoutbody);
            conn.connect();

            final byte[] buffer = new byte[16384];
            while (hasoutbody) {
                final int read = req.getInputStream().read(buffer);
                if (read <= 0) break;
                conn.getOutputStream().write(buffer, 0, read);
            }

            resp.setStatus(conn.getResponseCode());
            for (int i = 0; ; ++i) {
                final String header = conn.getHeaderFieldKey(i);
                if (header == null) break;
                final String value = conn.getHeaderField(i);
                resp.setHeader(header, value);
            }

            while (true) {
                final int read = conn.getInputStream().read(buffer);
                if (read <= 0) break;
                resp.getOutputStream().write(buffer, 0, read);
            }
        } catch (Exception e) {
            e.printStackTrace();
            // pass
        }
    }
}

Obviously this could use a bit of work with regard to error handling and the like but it was functional. I stopped using it, however, because it was easier in my case to make calls directly to the CLIENT_BACKEND than to deal with cookies, auth, etc. across two distinct domains.

Brian White
  • 8,332
  • 2
  • 43
  • 67
  • I tested it. However, the conn.getHeaderFieldKey(0) returns null. I have put i=1 so it works fine. https://docs.oracle.com/javase/7/docs/api/java/net/HttpURLConnection.html#getHeaderFieldKey(int) – Andrés Oviedo Feb 05 '19 at 12:01
  • Don't know what to say. I used this (briefly) and it worked just fine without as it is. I don't run it any longer so I can't do any further validation. – Brian White Feb 15 '19 at 19:33
  • I expect it works as-is on some boxes because of the following line in the document that Andrés Oviedo linked to: "Returns the key for the nth header field. Some implementations may treat the 0th header field as special, i.e. as the status line returned by the HTTP server. In this case, getHeaderField(0) returns the status line, but getHeaderFieldKey(0) returns null." – Trevor Mar 22 '22 at 13:54
14

I also needed to do the same, and after some non optimal with Spring controllers and RestTemplate, I found a better solution: Smiley's HTTP Proxy Servlet. The benefit is, it really does AS-IS proxying, just like Apache's mod_proxy, and it does it in a streaming way, without caching the full request/response in the memory.

Simply, you register a new servlet to the path you want to proxy to another server, and give this servlet the target host as an init parameter. If you are using a traditional web application with a web.xml, you can configure it like following:

<servlet>
    <servlet-name>proxy</servlet-name>
    <servlet-class>org.mitre.dsmiley.httpproxy.ProxyServlet</servlet-class>
    <init-param>
      <param-name>targetUri</param-name>
      <param-value>http://target.uri/target.path</param-value>
    </init-param>
</servlet>
<servlet-mapping>
  <servlet-name>proxy</servlet-name>
  <url-pattern>/mapping-path/*</url-pattern>
</servlet-mapping>

or, of course, you can go with the annotation config.

If you are using Spring Boot, it is even easier: You only need to create a bean of type ServletRegistrationBean, with the required configuration:

@Bean
public ServletRegistrationBean proxyServletRegistrationBean() {
    ServletRegistrationBean bean = new ServletRegistrationBean(
            new ProxyServlet(), "/mapping-path/*");
    bean.addInitParameter("targetUri", "http://target.uri/target.path");
    return bean;
}

This way, you can also use the Spring properties that are available in the environment.

You can even extend the class ProxyServlet and override its methods to customize request/response headers etc, in case you need.

Update: After using Smiley's proxy servlet for some time, we had some timeout issues, it was not working reliably. Switched to Zuul from Netflix, didn't have any problems after that. A tutorial on configuring it with Spring Boot can be found on this link.

Utku Özdemir
  • 7,390
  • 2
  • 52
  • 49
13

Unfortunately there is no easy way to do this. Basically you'll have to reconstruct the request, including:

  • correct HTTP method
  • request parameters
  • requests headers (HTTPUrlConnection doesn't allow to set arbitrary user agent, "Java/1.*" is always appended, you'll need HttpClient)
  • body

That's a lot of work, not to mention it won't scale since each such proxy call will occupy one thread on your machine.

My advice: use raw sockets or and intercept HTTP protocol on the lowest level, just replacing some values (like Host header) on the fly. Can you provide more context, why so you need this?

Tomasz Nurkiewicz
  • 334,321
  • 69
  • 703
  • 674
  • 1
    I got a client, an intermediate server and couple of main servers. The client only talks to the intermediate server which dispatches his call to a server. The server returns a response to the intermediate server, which he then process, and then returns the response to the client. – user1144031 Aug 26 '12 at 15:50
  • shouldn't copying request headers(3) and body(4) make up for copying request parameters(2) as well(since post parameters are part of the request body, for get they would be part of url)? Would it be reduntant(in http request) if I perform both the steps – mickeymoon Dec 09 '13 at 13:29
8
@RequestMapping(value = "/**")
public ResponseEntity route(HttpServletRequest request) throws IOException {
    String body = IOUtils.toString(request.getInputStream(), Charset.forName(request.getCharacterEncoding()));
    try {
        ResponseEntity<Object> exchange = restTemplate.exchange(firstUrl + request.getRequestURI(),
                HttpMethod.valueOf(request.getMethod()),
                new HttpEntity<>(body),
                Object.class,
                request.getParameterMap());
        return exchange;
    } catch (final HttpClientErrorException e) {
        return new ResponseEntity<>(e.getResponseBodyAsByteArray(), e.getResponseHeaders(), e.getStatusCode());
    }
}
JustinKSU
  • 4,875
  • 2
  • 29
  • 51
gstackoverflow
  • 36,709
  • 117
  • 359
  • 710
2

If you are forced to use spring, please check the rest template method exchange to proxy requests to a third party service.

Here you can find a working example.

db80
  • 4,157
  • 1
  • 38
  • 38
1

Use Spring Cloud Gateway

pom.xml

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-gateway-mvc</artifactId>
    </dependency>  


<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2020.0.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Controller

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.mvc.ProxyExchange;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

@RestController
public class Proxy extends BaseController {

    private String prefix="/proxy";
    private String Base="localhost:8080/proxy";  //destination

    void setHeaders(ProxyExchange<?> proxy){
        proxy.header("header1", "val1"); //add additional headers
    }

    @GetMapping("/proxy/**")
    public ResponseEntity<?> proxyPath(@RequestParam MultiValueMap<String,String> allParams, ProxyExchange<?> proxy) throws Exception {
        String path = proxy.path(prefix); //extract sub path
        proxy.header("Cache-Control", "no-cache");
        setHeaders(proxy);

        UriComponents uriComponents =  UriComponentsBuilder.fromHttpUrl(Base + path).queryParams(allParams).build();
        return proxy.uri(uriComponents.toUri().toString()).get();
    }

    @PutMapping("/proxy/**")
    public ResponseEntity<?> proxyPathPut(ProxyExchange<?> proxy) throws Exception {
        String path = proxy.path(prefix);
        setHeaders(proxy);
        return proxy.uri(Base + path).put();
    }
Nicolai Nikolai
  • 400
  • 3
  • 10
  • What's your opinion about using Spring cloud gateway (which is reactive )based routing to connect a client and downstream that are both servlet based blocking implementations. – trial999 Mar 10 '22 at 00:04