Spring HATEOAS and Ribbon with Eureka - host name in created links - spring

I'm using Spring HATEOAS to build and access a REST service. The service registers itself with a eureka server, and I use Ribbon (via #LoadBalanced RestTemplate) to call it.
Per default, the requests are made for the hostname (in the Host header) for the resolved instance. This causes the LinkBuilder in Spring HATEOAS to generate links for this host. When Ribbon makes a request to follow a link, it tries to lookup the link host name in eureka again and of course gets no result.
What is the best level to address this situation? After receiving the links with the first result, I guess it would be acceptable to direct the immediate following requests to the same service instance, I still feel making all requests to the symbolic service name would be better.
Is it possible to configure Ribbon to make requests with that service name in the Host header (assuming no virtual hosting in the target service, that seems to be a valid assumption)?
Otherwise is it possible to set a canonical base URL for the link builder in HATEOAS? (defining all links as complete strings is not acceptable for me)
My solution/workaround for now is explicitly setting an X-Forwarded-Host header when requesting the root resource. This works, but is a bit verbose and cumbersome.
Traverson traverson = new Traverson(URI.create("http://photo-store/"), MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8, MediaTypes.HAL_JSON).setRestOperations(imageService);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("X-Forwarded-Host", "photo-store");
String original = this.traverson.follow("image:original").withHeaders(httpHeaders).asTemplatedLink().expand(photoId).getHref();
(Side question: can I override the Host header like that, or is it treated special in Ribbon or RestTemplate?)
I feel that there should be a more declarative or convention or configuration based way to deal with this. Is there?

I also faced same problem. I resolved it by adding interceptor which adds X-Forwarded-Host header to every request.
#Bean
#LoadBalanced
RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(singletonList((ClientHttpRequestInterceptor) (request, body, execution) -> {
request.getHeaders().add("X-Forwarded-Host", request.getURI().getHost());
return execution.execute(request, body);
}));
return restTemplate;
}

I am using my own linkBuilder which fetches the name of the service from the properties and uses that as a Hostname. This is pretty basic, but easy to use and implement. If you want to use the whole spring hateoas link discovery magic sauce, you need to do more. But right now I'm searching for a solution that fits better with Spring Hateoas 1.x myself, so maybe I'll find a better answer soon.
The quick and dirty way:
#Component
public class MyLinkBuilder {
#Value("${spring.application.name}")
private String servicename;
public Link getLink(String path) {
String root = "http://" + servicename;
return new Link(root + path);
}
public Link getLink(String path, String rel) {
return getLink(path, LinkRelation.of(rel));
}
public Link getLink(String path, LinkRelation rel) {
String root = "http://" + servicename;
return new Link(root + path, rel);
}
}

Related

How to test a POSTing method by using an embedded Webserver in Springboot?

I am searching for a way to test a method, which sends a POST request to an external service. The application will NOT be itself a consumable webservice, that is why I didn't implement the shown class below as #RestController, #Controller, #Service, whatever types there may be.
But I don't know how to call the method postNumberPlate() to send a request to an embedded webserver (started in/by/at the unit test) to make some assertions on it. I want to avoid, to install an external webserver.
In other words: Can I start an embedded webserver inside a unit-test and 'tell' it to accept my POST request to inspect and assert the contents?
I already did:
a massive Webresearch (2-3 days?)
read Howto's
check the springboot docs
use an embedded Jetty Server (somehow blocking loop)
declare the Application as Webapplication and setting random port to jetty
experiment with Mockito, MockMVC
read "How to unittest a class using RestTemplate offline?" and compared it to my case, but found,
that it's very old (8y),
I don't know how to implement the parent interface, which is pretty huge
that the question and answers are too generic to deduce a solution for my case
it's not answering the embedded testing webserver problem I included.
The Class to be tested:
public class RestfulClient {
private RestTemplate restTemplate = new RestTemplate();
private HttpHeaders headers = new HttpHeaders();
#Value("${kafkaeskadapter.uri}")
private String destinationURL;
public RestfulClient() {}
public ResponseEntity<String> postNumberPlate(String key, CamImage camImage) {
LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("numplate", camImage.getIdentifier());
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<LinkedMultiValueMap<String,
Object>>(map, headers);
ByteArrayResource resource = new ByteArrayResource(camImage.getData()) {
/**
* IMPORTANT!!! Otherwise I receive a BAD REQUEST
* #return
*/
#Override
public String getFilename() {
return camImage.getIdentifier() + ".png";
}
};
map.add("image", resource);
ResponseEntity<String> result = restTemplate.exchange(destinationURL, HttpMethod.POST,
requestEntity, String.class);
return result;
}
}
I hope I could clarify my question a bit.
A solution is to write a simple light-weight Webservice Endpoint and include it into your Run Configuration of your IDE. I made a separate mini project and would add further methods if needed, e.g. to accept different media.
Prior to run the actual unit tests, it is possible to configure the start of the Endpoint and return a meaningful ResponseEntity. The result can be inspected et voilà, assertions are possible.
A word about StackOverflow user arrogance: #Raedwald, after reading and trying, the answers in the linked question are not really helpful, but involve a lot of knowlegde about the stuff, and I have no one around of my colleagues, which could ever assist at programming. So it wasn't helpful to flag my question for deletion.

Invalid JWToken: kid is a required JOSE Header

I am trying to implement an Oauth2 Authorization Server with SpringBoot using this guide as a reference.
My keystore has a single key. I have successfully managed to create a JWToken (I can check it at jwt.io).
I have also a test Resource Server. When I try to access any endpoint I receive the following message:
{
"error": "invalid_token",
"error_description": "Invalid JWT/JWS: kid is a required JOSE Header"
}
The token really does not have a kid header but I can not figure out how to add it. I can only add data to its payload, using a TokenEnchancer. It also seems that I am not the first one with this issue.
Is there any way to add this header or, at least, ignore it at the resource server?
I've been working on an article that might help you out here:
https://www.baeldung.com/spring-security-oauth2-jws-jwk
So, to configure a Spring Security OAuth Authorization Server to add a JWT kid header, you can follow the steps of section 4.9:
create a new class extending the JwtAccessTokenConverter
In the constructor:
configure the parent class using the same approach you've been using
obtain a Signer object using the signing key you're using
override the encode method. The implementation will be the same as the parent one, with the only difference that you’ll also pass the custom headers when creating the String token
public class JwtCustomHeadersAccessTokenConverter extends JwtAccessTokenConverter {
private JsonParser objectMapper = JsonParserFactory.create();
final RsaSigner signer;
public JwtCustomHeadersAccessTokenConverter(KeyPair keyPair) {
super();
super.setKeyPair(keyPair);
this.signer = new RsaSigner((RSAPrivateKey) keyPair.getPrivate());
}
#Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
content = this.objectMapper.formatMap(getAccessTokenConverter().convertAccessToken(accessToken, authentication));
} catch (Exception ex) {
throw new IllegalStateException("Cannot convert access token to JSON", ex);
}
Map<String, String> customHeaders = Collections.singletonMap("kid", "my_kid");
String token = JwtHelper.encode(content, this.signer, this.customHeaders)
.getEncoded();
return token;
}
}
Then, of course, create a bean using this converter:
#Bean
public JwtAccessTokenConverter accessTokenConverter(KeyPair keyPair) {
return new JwtCustomHeadersAccessTokenConverter(keyPair);
}
Here I used a KeyPair instance to obtain the signing key and configure the converter (based on the example of the article), but you might adapt that to your configuration.
In the article I also explain the relevant endpoints provided by the Spring Security OAuth Authentication Server.
Also, regarding #Ortomala Lokni's comment, I wouldn't expect Spring Security OAuth to add any new features at this point. As an alternative, you probably can wait to have a look at Spring Security's Authorization Server features, planned to be released in 5.3.0
I managed to solve it by changing the parameter used to identify the URL where the clients will retrieve the pubkey.
On application.properties, instead of:
security.oauth2.resource.jwk.key-set-uri=http://{auth_server}/.well-known/jwks.json
I used:
security.oauth2.resource.jwt.key-uri=http://{auth_server}/oauth/token_key
If I understood correctly, the key-set-uri config points to an endpoint that presents a set of keys and there is the need for a kid. On the other side key-uri config points to an endpoint with a single key.

Create a Custom Spring Cloud Netflix Ribbon Client

I am using Spring Cloud Netflix Ribbon in combination with Eureka in a Cloud Foundry environment.
The use case I am trying to implement is the following:
I have a running CF application named address-service with several instances spawned.
The instances are registering to Eureka by the service name address-service
I have added custom metadata to service instances using
eureka.instance.metadata-map.applicationId: ${vcap.application.application_id}
I want to use the information in Eureka's InstanceInfo (in particular the metadata and how many service instances are available) for setting a CF HTTP header "X-CF-APP-INSTANCE" as described here.
The idea is to send a Header like "X-CF-APP-INSTANCE":"appIdFromMetadata:instanceIndexCalculatedFromNoOfServiceInstances" and thus "overrule" CF's Go-Router when it comes to load balancing as described at the bottom of this issue.
I believe to set headers, I need to create a custom RibbonClient implementation - i.e. in plain Netflix terms a subclass of AbstractLoadBalancerAwareClient as described here - and override the execute() methods.
However, this does not work, as Spring Cloud Netflix Ribbon won't read the class name of my CustomRibbonClient from application.yml. It also seems Spring Cloud Netflix wraps quite a bit of classes around the plain Netflix stuff.
I tried implementing a subclass of RetryableRibbonLoadBalancingHttpClient and RibbonLoadBalancingHttpClient which are Spring classes. I tried giving their class names in application.yml using ribbon.ClientClassName but that does not work. I tried to override beans defined in Spring Cloud's HttpClientRibbonConfiguration but I cannot get it to work.
So I have two questions:
is my assumption correct that I need to create a custom Ribbon Client and that the beans defined here and here won't do the trick?
How to do it properly?
Any ideas are greatly appreciated, so thanks in advance!
Update-1
I have dug into this some more and found RibbonAutoConfiguration.
This creates a SpringClientFactory which provides a getClient() method that is only used in RibbonClientHttpRequestFactory (also declared in RibbonAutoConfiguration).
Unfortunately, RibbonClientHttpRequestFactory hard-codes the client to Netflix RestClient. And it does not seem possible to override either SpringClientFactory nor RibbonClientHttpRequestFactory beans.
I wonder if this is possible at all.
Ok, I'll answer this question myself, in case someone else may need that in the future.
Actually, I finally managed to implement it.
TLDR - the solution is here: https://github.com/TheFonz2017/Spring-Cloud-Netflix-Ribbon-CF-Routing
The solution:
Allows to use Ribbon on Cloud Foundry, overriding Go-Router's load balancing.
Adds a custom routing header to Ribbon load balancing requests (including retries) to instruct CF's Go-Router to route requests to the service instance selected by Ribbon (rather than by its own load balancer).
Shows how to intercept load balancing requests
The key to understanding this, is that Spring Cloud has its own LoadBalancer framework, for which Ribbon is just one possible implementation. It is also important to understand, that Ribbon is only used as a load balancer not as an HTTP client. In other words, Ribbon's ILoadBalancer instance is only used to select the service instance from the server list. Requests to the selected server instances are done by an implementation of Spring Cloud's AbstractLoadBalancingClient. When using Ribbon, these are sub-classes of RibbonLoadBalancingHttpClient and RetryableRibbonLoadBalancingHttpClient.
So, my initial approach to add an HTTP header to the requests sent by Ribbon's HTTP client did not succeed, since Ribbon's HTTP / Rest client is actually not used by Spring Cloud at all.
The solution is to implement a Spring Cloud LoadBalancerRequestTransformer which (contrary to its name) is a request interceptor.
My solution uses the following implementation:
public class CFLoadBalancerRequestTransformer implements LoadBalancerRequestTransformer {
public static final String CF_APP_GUID = "cfAppGuid";
public static final String CF_INSTANCE_INDEX = "cfInstanceIndex";
public static final String ROUTING_HEADER = "X-CF-APP-INSTANCE";
#Override
public HttpRequest transformRequest(HttpRequest request, ServiceInstance instance) {
System.out.println("Transforming Request from LoadBalancer Ribbon).");
// First: Get the service instance information from the lower Ribbon layer.
// This will include the actual service instance information as returned by Eureka.
RibbonLoadBalancerClient.RibbonServer serviceInstanceFromRibbonLoadBalancer = (RibbonLoadBalancerClient.RibbonServer) instance;
// Second: Get the the service instance from Eureka, which is encapsulated inside the Ribbon service instance wrapper.
DiscoveryEnabledServer serviceInstanceFromEurekaClient = (DiscoveryEnabledServer) serviceInstanceFromRibbonLoadBalancer.getServer();
// Finally: Get access to all the cool information that Eureka provides about the service instance (including metadata and much more).
// All of this is available for transforming the request now, if necessary.
InstanceInfo instanceInfo = serviceInstanceFromEurekaClient.getInstanceInfo();
// If it's only the instance metadata you are interested in, you can also get it without explicitly down-casting as shown above.
Map<String, String> metadata = instance.getMetadata();
System.out.println("Instance: " + instance);
dumpServiceInstanceInformation(metadata, instanceInfo);
if (metadata.containsKey(CF_APP_GUID) && metadata.containsKey(CF_INSTANCE_INDEX)) {
final String headerValue = String.format("%s:%s", metadata.get(CF_APP_GUID), metadata.get(CF_INSTANCE_INDEX));
System.out.println("Returning Request with Special Routing Header");
System.out.println("Header Value: " + headerValue);
// request.getHeaders might be immutable, so we return a wrapper that pretends to be the original request.
// and that injects an extra header.
return new CFLoadBalancerHttpRequestWrapper(request, headerValue);
}
return request;
}
/**
* Dumps metadata and InstanceInfo as JSON objects on the console.
* #param metadata the metadata (directly) retrieved from 'ServiceInstance'
* #param instanceInfo the instance info received from the (downcast) 'DiscoveryEnabledServer'
*/
private void dumpServiceInstanceInformation(Map<String, String> metadata, InstanceInfo instanceInfo) {
ObjectMapper mapper = new ObjectMapper();
String json;
try {
json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(metadata);
System.err.println("-- Metadata: " );
System.err.println(json);
json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(instanceInfo);
System.err.println("-- InstanceInfo: " );
System.err.println(json);
} catch (JsonProcessingException e) {
System.err.println(e);
}
}
/**
* Wrapper class for an HttpRequest which may only return an
* immutable list of headers. The wrapper immitates the original
* request and will return the original headers including a custom one
* added when getHeaders() is called.
*/
private class CFLoadBalancerHttpRequestWrapper implements HttpRequest {
private HttpRequest request;
private String headerValue;
CFLoadBalancerHttpRequestWrapper(HttpRequest request, String headerValue) {
this.request = request;
this.headerValue = headerValue;
}
#Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.putAll(request.getHeaders());
headers.add(ROUTING_HEADER, headerValue);
return headers;
}
#Override
public String getMethodValue() {
return request.getMethodValue();
}
#Override
public URI getURI() {
return request.getURI();
}
}
}
The class is looking for the information required for setting the CF App Instance Routing header in the service instance metadata returned by Eureka.
That information is
The GUID of the CF application that implements the service and of which several instances exist for load balancing.
The index of the service / application instance that the request should be routed to.
You need to provide that in the application.yml of your service like so:
eureka:
instance:
hostname: ${vcap.application.uris[0]:localhost}
metadata-map:
# Adding information about the application GUID and app instance index to
# each instance metadata. This will be used for setting the X-CF-APP-INSTANCE header
# to instruct Go-Router where to route.
cfAppGuid: ${vcap.application.application_id}
cfInstanceIndex: ${INSTANCE_INDEX}
client:
serviceUrl:
defaultZone: https://eureka-server.<your cf domain>/eureka
Finally, you need to register the LoadBalancerRequestTransformer implementation in the Spring configuration of your service consumers (which use Ribbon under the hood):
#Bean
public LoadBalancerRequestTransformer customRequestTransformer() {
return new CFLoadBalancerRequestTransformer();
}
As a result, if you use a #LoadBalanced RestTemplate in your service consumer, the template will call Ribbon to make a choice on the service instance to send the request to, will send the request and the interceptor will inject the routing header. Go-Router will route the request to the exact instance that was specified in the routing header and not perform any additional load balancing that would interfere with Ribbon's choice.
In case a retry were necessary (against the same or one or more next instances), the interceptor would again inject the according routing header - this time for a potentially different service instance selected by Ribbon.
This allows you to use Ribbon effectively as the load balancer and de-facto disable load balancing of Go-Router, demoting it to a mere proxy. The benefit being that Ribbon is something you can influence (programmatically) whereas you have little to no influence over Go-Router.
Note: this was tested for #LoadBalanced RestTemplate's and works.
However, for #FeignClients it does not work this way.
The closest I have come to solving this for Feign is described in this post, however, the solution described there uses an interceptor that does not get access to the (Ribbon-)selected service instance, thus not allowing access to the required metadata.
Haven't found a solution so far for FeignClient.

How to redirect request to an URI passed in headers with Jetty AsyncProxyServlet

I'm creating a proxy micro-service with SpringBoot, Jetty and kotlin.
The purpose of this micro-service is to forward requests made by my front-end application to external services (avoiding CORS) and send back the response after checking some custom authentication. The query I'll receive will contain the URL of the target in the headers (i.e: Target-Url: http://domain.api/getmodel).
Based on this answer, I made a class that extends AsyncProxyServlet and overwrote the method sendProxyRequest :
class ProxyServlet : AsyncProxyServlet() {
private companion object {
const val TARGET_URL = "Target-Url"
}
override fun sendProxyRequest(clientRequest: HttpServletRequest, proxyResponse: HttpServletResponse, proxyRequest: Request) {
// authentication logic
val targetUrl = clientRequest.getHeader(TARGET_URL)
if (authSuccess) {
super.sendProxyRequest(clientRequest, proxyResponse, proxyRequest)
} else {
proxyResponse.status = HttpStatus.UNAUTHORIZED.value()
}
}
}
When I query my proxy, I get in this method and successfuly authenticate, but I fail to understand how to use my targetUrl to redirect the request.
The method keeps calling itself as it's redirecting the original request to itself (the request from http://myproxy:port/ to http://myproxy:port/).
It is very difficult to find documentation on this specific implementation of jetty, StackOverflow is my last resort!
First, setup logging for Jetty, and configure DEBUG level logging for the package namespace org.eclipse.jetty.proxy, this will help you understand the behavior much better.
The Request proxyRequest parameter represents a HttpClient/Request object, which is created with an immutable URI/URL destination (this is due to various other features that requires information from the URI/URL such as Connection pooling, Cookies, Authentication, etc), you cannot change the URI/URL on this object after the fact, you must create the HttpClient/Request object with the correct URI/URL.
Since all you want to do is change the target URL, you should instead be overriding the method ...
protected String rewriteTarget(HttpServletRequest clientRequest)
... and returning the new absolute URI String to the destination that you want to use (The "Target-Url" header in your scenario looks like a good candidate)
You can see this logic in the ProxyServlet.service(HttpServletRequest, HttpServletResponse) code block (which AsyncProxyServlet extends from)

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
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.
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.
ProxyPass /placesapi-packaged-war-1.0.0-SNAPSHOT
ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/
ProxyPassReverse /placesapi-packaged-war-1.0.0-SNAPSHOT
ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/

Resources