Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to control the web context of Spring Hateoas generated links?

We are building an API and are using Spring RestControllers and Spring HATEOAS.
When the war file is deployed to a container and a GET request is made to http://localhost:8080/placesapi-packaged-war-1.0.0-SNAPSHOT/places, the HATEOAS links look like this:

{
  "links" : [ {
    "rel" : "self",
    "href" : "http://localhost:8080/placesapi-packaged-war-1.0.0-SNAPSHOT/places",
    "lastModified" : "292269055-12-02T16:47:04Z"
  } ]
}

in that the web context is that of the deployed application (eg: placesapi-packaged-war-1.0.0-SNAPSHOT)

In a real runtime environment (UAT and beyond), the container is likely to be sat behind a http server such as Apache where a virtual host or similar fronts the web application. Something like this:

<VirtualHost Nathans-MacBook-Pro.local>
   ServerName Nathans-MacBook-Pro.local

   <Proxy *>
     AddDefaultCharset Off
     Order deny,allow
     Allow from all
   </Proxy>

   ProxyPass / ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/
   ProxyPassReverse / ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/

</VirtualHost>

Using the above, when we make a GET request to http://nathans-macbook-pro.local/places, the resultant response looks like this:

{
  "links": [ {
    "rel": "self",
    "href": "http://nathans-macbook-pro.local/placesapi-packaged-war-1.0.0-SNAPSHOT/places",
    "lastModified": "292269055-12-02T16:47:04Z"
  } ]
}

It's wrong because the link in the response contains the web app context, and if a client were to follow that link they would get a 404

Does anyone know how to control the behaviour of Spring HATEOAS in this respect? Basically I need to be able to control the web context name that it generates within links.

I did a bit of poking around and can see that with a custom header X-Forwarded-Host you can control the host and port, but I couldn't see anything similar to be able to control the context.

Other options we've considered involve either deploying the app to the ROOT context or to a fixed named context, and then set up our virtual host accordingly. However, these feel like compromises rather than solutions because ideally we would like to host several versions of the application on the same container (eg: placesapi-packaged-war-1.0.0-RELEASE, placesapi-packaged-war-1.0.1-RELEASE, placesapi-packaged-war-2.0.0-RELEASE etc) and have the virtual host forward to the correct app based on http request header.

Any thoughts on this would be very much appreciated,
Cheers

Nathan

like image 290
Nathan Russell Avatar asked Sep 29 '14 08:09

Nathan Russell


2 Answers

First, in case you weren't aware, you can control the context of the web application (under Tomcat at least) by creating webapp/META-INF/context.xml containing the line:

<Context path="/" />

... which will make set the application context to be the same as what you are using (/).

However, that wasn't your question. I posed a similar question a little while back. As a result, from what I can gather, there's no out-of-the-box mechanism for controlling the generated links manually. Instead I created my own modified version of ControllerLinkBuilder, which built up the base of the URL using properties defined in application.properties. If setting the context on your application itself is not an option (i.e. if you're running multiple versions under the same Tomcat instance) then I think that this is your only option, if ControllerLinkBuilder is not building up your URLs correctly.

like image 69
Steve Avatar answered Oct 12 '22 23:10

Steve


Had a very similar problem. We wanted our public URL to be x.com/store and internally our context path for hosts in a cluster was host/our-api. All the URLS being generated contained x.com/our-api and not x.com/store and were unresolvable from the public dirty internet.

First just a note, the reason we got x.com was because our reverse-proxy does NOT rewrite the HOST header. If it did we'd need to add an X-Forwarded-Host header set to x.com so HATEOAS link builder would generate the correct host. This was specific to our reverse-proxy.

As far as getting the paths to work...we did NOT want to use a custom ControllerLinkBuilder. Instead we rewrite the context in a servlet filter. Before i share that code, i want to bring up the trickiest thing. We wanted our api to generate useable links when going directly to the tomcat nodes hosting the war, thus urls should be host/our-api instead of host/store. In order to do this the reverse-proxy needs to give a hint to the web app that the request came through the reverse-proxy. You can do this with headers, etc. Specifically for us, we could ONLY modify the request url, so we changed our load balancer to rewrite x.com/store to host/our-api/store this extra /store let us know that the request came through the reverse-proxy, and thus needed to be using the public context root. Again you can use another identifier (custom header, presence of X-Forwared-Host, etc) to detect the situation..or you may not care about having individual nodes give back usable URLs (but it's really nice for testing).

public class ContextRewriteFilter extends GenericFilterBean {


    @Override
    public void doFilter(ServletRequest req, ServletResponse res, final FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest)req;

        //There's no cleanup to perform so no need for try/finally
        chain.doFilter(new ContextRewriterHttpServletRequestWrapper(request), res);

    }


    private static class ContextRewriterHttpServletRequestWrapper extends HttpServletRequestWrapper {

        //I'm not totally certain storing/caching these once is ok..but i can't think of a situation
        //where the data would be changed in the wrapped request
        private final String context;
        private final String requestURI;
        private final String servletPath;



        public ContextRewriterHttpServletRequestWrapper(HttpServletRequest request){
            super(request);

            String originalRequestURI = super.getRequestURI();
            //If this came from the load balancer which we know BECAUSE of the our-api/store root, rewrite it to just be from /store which is the public facing context root
            if(originalRequestURI.startsWith("/our-api/store")){
                requestURI = "/store" + originalRequestURI.substring(25);
            }
            else {
                //otherwise it's just a standard request
                requestURI = originalRequestURI;
            }


            int endOfContext = requestURI.indexOf("/", 1);
            //If there's no / after the first one..then the request didn't contain it (ie /store vs /store/)
            //in such a case the context is the request is the context so just use that
            context = endOfContext == -1 ? requestURI : requestURI.substring(0, endOfContext);

            String sPath = super.getServletPath();
            //If the servlet path starts with /store then this request came from the load balancer
            //so we need to pull out the /store as that's the context root...not part of the servlet path
            if(sPath.startsWith("/store")) {
                sPath = sPath.substring(6);
            }

            //I think this complies with the spec
            servletPath = StringUtils.isEmpty(sPath) ? "/" : sPath;


        }


        @Override
        public String getContextPath(){
            return context;
        }

        @Override
        public String getRequestURI(){

            return requestURI;
        }

        @Override
        public String getServletPath(){
            return servletPath;
        }

    }
}

It's a hack, and if anything depends on knowing the REAL context path in the request it will probably error out...but it's been working nicely for us.

like image 25
Chris DaMour Avatar answered Oct 13 '22 01:10

Chris DaMour