user8681
user8681

Reputation:

CORS headers on OPTIONS request with Jersey

I have a REST API where I want some methods to have specific CORS headers. I have an annotation on the resource method, and a filter to add the headers:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface CorsHeaders {}

@Path("api")
class MyApi {
  @CorsHeaders
  @GET
  public Response m() {
    return Response.ok().build();
  }
}

@Provider
class CorsFilter implements ContainerResponseFilter {
  @Context private ResourceInfo resourceInfo;

  @Override 
  public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
    if (resourceInfo.getResourceMethod().getAnnotation(CorsHeaders.class) != null) {
      responseContext.getHeaders().add(/* appropriate headers here*/);
    }
  }
}

This works well for all GET, POST, etc. requests. It does not work for OPTIONS requests, because the resource method will resolve to org.glassfish.jersey.server.wadl.processor.WadlModelProcessor$OptionsHandler instead of my method, and so the annotation will not be present.

I can work around this by adding a @OPTIONS @CorsHeaders public Response options() { return Response.ok().build(); } method to my API class (on the same @Path), but I'd rather not have to do that for all methods.

How can I find out the actual (GET/POST) resource method when handling an OPTIONS request?

Upvotes: 8

Views: 2588

Answers (1)

Fencer
Fencer

Reputation: 1088

I'm afraid what you trying to accomplish is actually not possible in a nice way using the current version without changing Jersey itself.

Anyway I'm also not really sure if using @Provider for request specific filters is the right way according to specification specification. But who am I to speak I actually do it myself. Of course one could also register the filter in a ResourceConfig. In general I'd suggest to take a look at @NameBinding, but for this case name-binding Jersey-style is not enough. With @NameBinding you do not have to check for the annotation yourself, because Jersey already does that for you.

Unfortunately again with using @NameBinding, which was introduced just for such cases there is the problem of the "auto-generated" OPTIONS-handler. I did quite some digging (some of the most relevant classes/methods are OptionsMethodProcessor, WadlModelProcessor, ResourceModelConfigurator#init and ServerRuntime ApplicationHandler#initialize) but did not find a way to hook into the process adequately. Here's what should suffice for handling CORS:

@NameBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface CrossOrigin {
}


@CrossOrigin
public class CrossOriginResponseFilter implements ContainerResponseFilter {
    public void filter(ContainerRequestContext requestContext,  
                       ContainerResponseContext responseContext)
    throws IOException {
        // do Cross Origin stuff
    }
}

@Path("ress")
public class MyResource {
    @CrossOrigin
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response save(DetailsDTO details) {
         // do something with the details
    }
}

But while this works well for any direct request to the resource this does also not work for CORS-preflight-requests, because Jersey does not apply the name-binding-annotation @CrossOrigin to the predefined / auto-generated OPTIONS-handler. You can see that when looking at the runtime-representation of the Resource in the request context (don't let all the text irritate you, the important thing are the nameBindings-properties at the end of each ResourceMethod):

[ResourceMethod{
    httpMethod=POST, consumedTypes=[application/json], 
    producedTypes=[application/json], suspended=false, suspendTimeout=0, 
    suspendTimeoutUnit=MILLISECONDS, invocable=Invocable{handler=ClassBasedMethodHandler{handlerClass=class de.example.MyResource, 
    handlerConstructors=[org.glassfish.jersey.server.model.HandlerConstructor@2c253414]}, definitionMethod=public javax.ws.rs.core.Response de.example.MyResource.save(de.example.DetailsDTO),
    parameters=[Parameter [type=class de.example.DetailsDTO, source=null, defaultValue=null]],
    responseType=class javax.ws.rs.core.Response},
    nameBindings=[interface de.example.CrossOrigin]},
ResourceMethod{
    httpMethod=OPTIONS, consumedTypes=[*/*], 
    producedTypes=[application/vnd.sun.wadl+xml], suspended=false, 
    suspendTimeout=0, suspendTimeoutUnit=MILLISECONDS, 
    invocable=Invocable{handler=ClassBasedMethodHandler{handlerClass=class org.glassfish.jersey.server.wadl.processor.WadlModelProcessor$OptionsHandler, 
    handlerConstructors=[org.glassfish.jersey.server.model.HandlerConstructor@949030f]}, 
    definitionMethod=public abstract java.lang.Object org.glassfish.jersey.process.Inflector.apply(java.lang.Object), 
    parameters=[Parameter [type=interface javax.ws.rs.container.ContainerRequestContext, source=null, defaultValue=null]], responseType=class javax.ws.rs.core.Response},
    nameBindings=[]},
ResourceMethod{
    httpMethod=OPTIONS, consumedTypes=[*/*], producedTypes=[text/plain], 
    suspended=false, suspendTimeout=0, suspendTimeoutUnit=MILLISECONDS, 
    invocable=Invocable{handler=ClassBasedMethodHandler{handlerClass=class org.glassfish.jersey.server.wadl.processor.OptionsMethodProcessor$PlainTextOptionsInflector,
    handlerConstructors=[]}, definitionMethod=public abstract java.lang.Object org.glassfish.jersey.process.Inflector.apply(java.lang.Object), 
    parameters=[Parameter [type=interface javax.ws.rs.container.ContainerRequestContext, source=null, defaultValue=null]], 
    responseType=class javax.ws.rs.core.Response}, nameBindings=[]},
ResourceMethod{
    httpMethod=OPTIONS, consumedTypes=[*/*], producedTypes=[*/*], 
    suspended=false, suspendTimeout=0, suspendTimeoutUnit=MILLISECONDS, 
    invocable=Invocable{handler=ClassBasedMethodHandler{handlerClass=class org.glassfish.jersey.server.wadl.processor.OptionsMethodProcessor$GenericOptionsInflector,
    handlerConstructors=[]}, definitionMethod=public abstract java.lang.Object org.glassfish.jersey.process.Inflector.apply(java.lang.Object), 
    parameters=[Parameter [type=interface javax.ws.rs.container.ContainerRequestContext, source=null, defaultValue=null]], responseType=class javax.ws.rs.core.Response}, 
    nameBindings=[]}]

But now you can use the name-binding information to handle the preflight requests yourself by creating another filter:

@Provider
@Priority(1)
public class CrossOriginResponseFilter implements ContainerRequestFilter {
    Resource res = ((ContainerRequest)requestContext)
        .getUriInfo().getMatchedResourceMethod().getParent();

    if (res.getResourceMethods().get(0).getNameBindings().contains(CrossOrigin.class)) {
        // handlePreflightRequest and abort: requestContext.abortWith(builder.build());
    }
}

Funny thing is that the extracted Resource res will only contain the relevant resource method that matches the actual request URI and method and the auto-generated OPTIONS-handlers as you can see above in the run-time representation of the resource methods. The example resource actually has further methods, POSTs and GETs. So you can access the needed information by using .get(0) here.

BUT BEWARE! I did not check if that is true in any case or just when you for example annotate your resource methods with separate paths. So maybe there is more matching work to do than in my simple version here.

I myself find that solution to be quite ugly and ended up with a filter that is not method-specific but simply handles all requests to any resource (similar to the solution of the guys here). But it should be an answer to the question how you can "find out the actual (GET/POST) resource method when handling an OPTIONS request".

Upvotes: 5

Related Questions