Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 530
Jorn Avatar asked Jun 24 '16 09:06

Jorn


People also ask

What is an OPTIONS request in Cors?

This is an OPTIONS request that is made to the server, prior to the actual request being made. It will contain different Access-Control-XX-XX headers, and the server should respond to those headers with its own CORS response headers. Here are the matching headers:

How do I enable Cors in JAX-RS?

2. How to Enable CORS Mechanism There are two ways by which we can enable CORS in JAX-RS. The first and the most basic way is to create a filter to inject necessary response header at run-time in every request. The other one is to manually add an appropriate header in each URL endpoint.

What is cors and how does it work?

CORS also relies on a mechanism by which browsers make a “preflight” request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request.

How to handle Cors with Jersey?

With Jersey, to handle CORS, you can just use a ContainerResponseFilter. The ContainerResponseFilter for Jersey 1.x and 2.x are a bit different. Since you haven't mentioned which version you're using, I'll post both.


1 Answers

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".

like image 191
Fencer Avatar answered Oct 23 '22 10:10

Fencer