bbilger/jrestless

Unexpected application root path behaviour

Closed this issue · 11 comments

Currently the following combination :

  • API Gateway resource path = "/api/{+proxy}"
  • JAX-RS Resource class @path("/helloworld")

Does not resolve/work. It is expecting the root resource classes to have the same path prefix as the AG resource path (so @path("/api/..") in this example). This is unexpected. The combination above should result in :

https://mysite.com/{stage}/api/helloworld

Since the configuration implies the jersey application route is served by the {+proxy} section. It would be good if the Application instance root path is configurable (e.g. through @ApplicationPath which does not seem to be respected) or if the default is changed.

Hi @remonvv ,
You can already configure the baseUri:

public class YourRequestHandler extends GatewayRequestObjectHandler {
  public YourRequestHandler() {
    super(URI.create("/api/")); // use a trailing slash!
    ...
    init(config);
    start();
  }
}

Does this solve your problem?

Supporting @ApplicationPath might be an idea for the future but I never saw it being supported by any container except servlet containers; I will re-check.

I am not sure if I understand what you mean by "or if the default is changed". One can have multiple API Gateway resource paths e.g. "/api1/{+proxy}" , "/api2/{+proxy}", ... or even "/api3/some/thing" which all invoke the same handler/function, so it's difficult to assume the baseUri automatically.

Hi @bbilger

That does address it somewhat perhaps.

So, my expectation was/is that the JAX-RS resources have as their root the base URI of the API Gateway/Lamba proxy endpoint. So in your example I'd expect "api1/api2/api3" to be separate functions/containers defined in serverless.yml for example rather than having to be defined in @path, especially since most services consist of many JAX-RS resource classes that'd all have to prefix their @path with "api1/2/3" in your example (if i understand correctly). Basically I'd expect the {+proxy} section of the path to be JAX-RS container root rather than having them overlap :

How it is now :
path: api/{+proxy) + @path("/api/auth") -> reachable at https://domain/api/auth
path: api/{+proxy) + @path("/auth") -> unreachable

Expectation :
path: api/{+proxy) + @path("/api/auth") -> reachable at https://domain/api/api/auth
path: api/{+proxy) + @path("/auth") -> reachable at https://domain/api/auth

Right now it feels like I'm defining part of the path twice. Hope that makes sense.

Hi @remonvv

So in your example I'd expect "api1/api2/api3" to be separate functions/containers defined in serverless.yml

No that's not the case. See for example: https://github.com/bbilger/jrestless-examples/blob/master/aws/gateway/aws-gateway-security-cognito-authorizer/serverless.yml In the example 4 APIGW endpoints are defined: api/public, api/private, ui, ui/{proxy+} and all of them use the same function/container. So there really is no way to "assume" the base URI, here.

Well, you can define the base URI for a resource (class), so you don't have to repeat it on all endpoints (method). See for example https://github.com/bbilger/jrestless-examples/blob/master/aws/gateway/aws-gateway-security-cognito-authorizer/src/main/java/com/jrestless/aws/examples/ApiResource.java

If you are sure about your base URI, I suggest you set it in the handler as suggested above: super(URI.create("/api/")); So you no longer have to mention "api" on the resource or endpoint.

I can't think of any other solutions, here, that I could implement in the framework as a general solution since the function (handler, container, resources, endpoints) don't have any clue about APIGW endpoints defined in serverless.yml

@remonvv

If you are sure about your base URI, I suggest you set it in the handler as suggested above: super(URI.create("/api/")); So you no longer have to mention "api" on the resource or endpoint.

Scratch this! This is not doing what it is supposed to do. I think there's a bug; I'll check. (the other points are still valid, though)

What do you think about the following, @remonvv ?

import java.io.IOException;
import java.net.URI;

import javax.inject.Inject;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.Application;

@PreMatching
public class ApplicationPathFilter implements ContainerRequestFilter {
  private static final URI ROOT_URI = URI.create("/");
  private final URI applicationPathUri;
  @Inject
  ApplicationPathFilter(Application applicationConfig) {
    ApplicationPath ap = applicationConfig.getClass().getAnnotation(ApplicationPath.class);
    if (ap != null) {
      String path = ap.value();
      path = path.trim();
      if (!path.startsWith("/")) {
        path = "/" + path;
      }
      if (path.endsWith("/")) {
        path = path.substring(0, path.length() - 1);
      }
      if (path.endsWith("/*")) {
        path = path.substring(0, path.length() - 2);
      }
      applicationPathUri = URI.create(path);
    } else {
    	applicationPathUri = null;
    }
  }
  @Override
  public void filter(ContainerRequestContext requestContext) throws IOException {
    if (applicationPathUri != null) {
      URI requestUri = requestContext.getUriInfo().getRequestUri();
      URI newUri = applicationPathUri.relativize(requestUri);
      newUri = ROOT_URI.resolve(newUri);
      requestContext.setRequestUri(newUri);
    }
  }
}

You would use if as follows for APIGW:

@ApplicationPath("api")
public class YourConfig extends ResourceConfig {
  public YourConfig() {
    register(ApplicationPathFilter.class);
    register(GatewayFeature.class);
    ...
  }
}
public class RequestHandler extends GatewayRequestObjectHandler {
  public RequestHandler() {
    ResourceConfig config = new YourConfig();
    init(config);
    start();
  }
}

@bbilger That's seems like a viable solution to implement my requirement but I think the core design issue remains unaddressed. I think we're having a disconnect about the core problem I'm having. I am short on time now but I'll respond with my thoughts on this today.

Hi @remonvv
I think I understand now that you don't want to repeat yourself. So what do you think about the following?

Some background:
GatewayRequest#getResource returns what is configured in APIGW e.g. /api/{proxy+}. The requestUri will be e.g. /api/auth. This filter will set the requestUri to /auth and should hopefully fulfill your needs. Note: I didn't test it extensively.

import java.io.IOException;
import java.net.URI;

import javax.annotation.Priority;
import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;

import com.jrestless.aws.gateway.io.GatewayRequest;

@PreMatching
@Priority(0)
public class DynamicApplicationPathFilter implements ContainerRequestFilter {

  private static final String PROXY_SUFFIX = "/{proxy+}";
  private static final URI ROOT_URI = URI.create("/");

  private final GatewayRequest gatwayRequest; // this is a proxy and it is request-scoped

  @Inject
  DynamicApplicationPathFilter(GatewayRequest gatwayRequest) {
    this.gatwayRequest = gatwayRequest;
  }

  @Override
  public void filter(ContainerRequestContext requestContext) throws IOException {
    if (isProxyResourceWithApplicationPath()) {
      URI requestUri = requestContext.getUriInfo().getRequestUri();
      URI newUri = getDynamicApplicationPath().relativize(requestUri);
      newUri = ROOT_URI.resolve(newUri);
      requestContext.setRequestUri(newUri);
    }
  }

  private boolean isProxyResourceWithApplicationPath() {
    String resource = gatwayRequest.getResource();
    if (resource == null) {
      return false;
    } else if (PROXY_SUFFIX.equals(resource)) {
      return false;
    } else {
      return resource.endsWith(PROXY_SUFFIX);
    }
  }

  private URI getDynamicApplicationPath() {
    String resource = gatwayRequest.getResource();
    return URI.create(resource.substring(0, resource.length() - PROXY_SUFFIX.length()));
  }
}

Let me know if this works for you.

I can add this to the framework if you need it but I don't want to enable it by default since in my opinion there are too many cases where this behavior is not accurate/desired. You would be able to enable it by registering it in your ResourceConfig.

Hi @bbilger

Do you have Skype or some way of real-time coms? I think there's either a misunderstanding or a bug. For example, if I checkout and deploy the usage-example project it does not work (the endpoint isn're reachable at {stage}/health/sample) which I suspect is a side effect of the issue/concern I'm talking about ( serverless.yml - http: ANY / does not seem to result in a reachable endpoint regardless of Jersey/@path config)

Hi @remonvv,
You can reach me via skype after 20:00 CET (this week Tuesday, Wednesday and Sunday, only)

created #24 and #23 to introduce the filters discussed, here (DynamicApplicationPathFilter, ApplicationPathFilter)
closing this ticket

Hi @remonvv ,
it's been a while (have been quite busy; sorry) but in case you are still interested, JRestless should support what you need, now, either by using DynamicProxyBasePathFilter (which must be registered explicitly) or by using a custom domain configured accordingly in APIGW.
I updated the way JRestless handles the base- and request URI in v0.5.0 and it's described here: https://github.com/bbilger/jrestless/blob/master/docs/src/docs/asciidoc/uri_handling.adoc