9

Spring HATEOAS provides the handy ControllerLinkBuilder to create links to controller methods, which will be added as hrefs in the JSON/XML returned to a client. For instance:

resource.add(linkTo(methodOn(FooController.class)
    .findFoo(entity.getClient().getId()))
    .withRel("show"));

... might generate JSON a bit like:

{
  "name":"foo", 
  "links":[
    {"rel":"show","href":"http://111.11.11.111:28080/foos/1"}
  ]
}

However...

I tend to access my services through a reverse proxy. Which I guess most people probably would. This lets me have multiple services running on different ports, but lets me access them all through the same base URL. Unfortunately, accessing through a proxy means that the URL being generated by Spring HATEOAS is not a URL which is valid for accessing the resource.

Now I could just hard-code the links, but that's rather fragile. Having the ControllerLinkBuilder generate URLs based on my controller @RequestMapping configuration is valuable to me, as it avoids the risk of my links getting out of sync with reality.

So I was wondering whether there's a property somewhere that I could use to force the host and port values. I'm using Spring Boot, so ideally a property that I could add to the application.properties file in each environment.

Note:

As this issue seems to be caused by a bug in Spring, I should probably point out that I'm using Spring Boot 1.0.2.RELEASE.

Steve
  • 9,270
  • 5
  • 47
  • 61
  • Working through the code of the `ControllerLinkBuilder`, it looks like it ought to be using the `X-Forwarded-Host` header to generate the link. I'll do a bit more testing, but it may well be that my links are incorrect because the header is not being set correctly by my reverse proxy. – Steve Jun 12 '14 at 09:47
  • If you use embedded Tomcat with Spring Boot it will set up a valve to populate the header by default. If you are deploying to an existing container you would have to set up the valve yourself. – Dave Syer Jun 12 '14 at 11:08
  • I'm using the Spring Boot embedded Tomcat. I have now confirmed that there's an `X-Forwarded-Host` header being set with no port. The host portion is being set correctly, so I'm trying to debug the `ControllerLinkBuilder` to establish why it seems to be appending the `server.port` value instead of leaving it off. – Steve Jun 12 '14 at 11:42
  • It's looking like a bug in `ServletUriComponentsBuilder` which has also been copied into the code for `ControllerLinkBuilder`. I'll post an answer with the details. – Steve Jun 12 '14 at 12:23

2 Answers2

5

A pure answer to the question I originally posed seems to involve writing my own ControllerLinkBuilder implementation which has the option of building the URL based on environment variables that I set. I may do that.

However, the reason I was trying to force the URL is that there is a bug in the ControllerLinkBuilder. It's worth noting that this bug is a bug in code which was copied from ServletUriComponentsBuilder.

String scheme = request.getScheme();

// The port number retrieved here is the port set by server.port
int port = request.getServerPort();
String host = request.getServerName();

String header = request.getHeader("X-Forwarded-Host");

if (StringUtils.hasText(header)) {
    String[] hosts = StringUtils.commaDelimitedListToStringArray(header);
    String hostToUse = hosts[0];
    if (hostToUse.contains(":")) {
        String[] hostAndPort = StringUtils.split(hostToUse, ":");
        host  = hostAndPort[0];

        // Note that the port is set if there is a ":" in the address.
        port = Integer.parseInt(hostAndPort[1]);
    }
    else {
        host = hostToUse;
    }
}

ServletUriComponentsBuilder builder = new ServletUriComponentsBuilder();
builder.scheme(scheme);
builder.host(host);

// Here lies the bug...
if ((scheme.equals("http") && port != 80) || (scheme.equals("https") && port != 443)) {
    builder.port(port);
}

Basically, the port is only set when the server.port is not 80 or 443, rather than being based on the port used for the request. This means that if the X-Forwarded-Host is using a default port for the scheme (and therefore not having anything after the ":"), then the application port will be used instead of the default.

Steve
  • 9,270
  • 5
  • 47
  • 61
2

Spring-Boot uses an older version of Spring-HATEOAS, i think it was .11 that they added support for X-Forwarded-Port and X-Forwarded-Ssl headers, just add that explicit version to your POM and if your proxy is doing the right thing and adding those headers you should be good to go.

Also if your proxy can be configured to NOT rewrite the HOST header the built in controller link builder will work just fine.

Chris DaMour
  • 3,650
  • 28
  • 37
  • Unfortunately the issue is a bug in Spring MVC (https://jira.spring.io/browse/SPR-11872) which has also been duplicated in HATEOAS (a bit of copy-paste re-use ;) ). The bug exists in the latest source. – Steve Jun 23 '14 at 08:11