Best way to implement method OPTIONS in REST services

17,005

Solution 1

You don't need to implements the OPTIONS HTTP VERB in this case. Since you're using RESTEasy, which is the JAX-RS implementation used by Wildfly, the issue I encountered was due to the servlet-mapping on web.xml.

I have encountered this when I added the JAX-RS facet on Eclipse and tell it to update the web.xml. The default generated web.xml containing the Restful application mapping doesn't map your application properly to your RESTful resource path.

This is how the web.xml should look like, provided you have not created your own custom Application.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    version="3.1">
    <display-name>My REST API</display-name>
    <description>My REST API</description>
    <servlet>
        <description>JAX-RS Tools Generated - Do not modify</description>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <url-pattern>/jaxrs/*</url-pattern>
    </servlet-mapping>
</web-app>

Make sure that your <servlet-name> and <servlet-mapping> are mapped as in the example above. If you extended the Application class, just specify it in your web.xml instead of the default Application as shown above.

Also, your @POST resource method, it's recommended to specify the resource type of your RESTful data (in your case, your DTO) using @Consumes annotation.

Eg.

@POST
@Path("/save")
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Response save(CervejaDTO cervejaDTO)

}

Solution 2

I tried RestEasy's CorsFilter but calls made with the OPTIONS method were returning

RESTEASY003655: No resource method found for options, return OK with Allow header

I wrote a simple filter that:

  1. Makes sure the CORS header you need are applied to the response.
  2. Returns the HTTP status code 200 when calling an endpoint with the OPTIONS method. You just tell the client that its CORS preflight requests was accepted.

Here is the code. This is a simplified version, ditry - yet efficient. Feel free to refine the filter if you only want to send back a 200 when querying a "real" endpoint.

@Provider 
public class CorsFilter implements ContainerResponseFilter {  

  @Override
  public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
    MultivaluedMap<String, Object> headers = responseContext.getHeaders();
    headers.add("Access-Control-Allow-Origin", "*"); // If you want to be more restrictive it could be localhost:4200
    headers.add("Access-Control-Allow-Methods", "GET, PUT, POST, OPTIONS"); // You can add HEAD, DELETE, TRACE, PATCH
    headers.add("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept, Accept-Language"); // etc

    if (requestContext.getMethod().equals("OPTIONS"))
        responseContext.setStatus(200);
}}

From this post and my preferred CORS explanation.

Solution 3

"however, when I implement the POST method, it says that I don't have the OPTIONS method implemented for it."

"When i make a POST or DELTE request, the application make automatically a OPTIONS request before"

This definitely sound like a CORS (Cross Origin Resource Sharing) problem. You can read more about it at HTTP access control (CORS). Basically the OPTIONS request is preflight request before the actual request. This will happen for certain types of AJAX requests.

For this, RESTeasy has the CorsFilter you can register. You need to configure the filter to the settings you want to allow. Also see an example here for one way to configure it.

Solution 4

You can use @Path("{path:.*}").

@OPTIONS
@Path("{path:.*}")
public Response handleCORSRequest() throws Exception {
    Response.ResponseBuilder builder = Response.ok();
    return builder.build();
}

Solution 5

For me that was the only way to do work.

Create the class in your java restclient project.

    import javax.ws.rs.core.Context;
    import javax.ws.rs.core.HttpHeaders;
    import javax.ws.rs.core.Response;
    import javax.ws.rs.core.Response.ResponseBuilder;
    import javax.ws.rs.ext.ExceptionMapper;
    import javax.ws.rs.ext.Provider;

    import org.jboss.resteasy.spi.DefaultOptionsMethodException;

    @Provider
    public class OptionsHander implements
         ExceptionMapper<DefaultOptionsMethodException> {

    private static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
    private static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";

    private static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
    private static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
    private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";

    private static final String ACCESS_CONTROL_ALLOW_ORIGIN_ANYONE = "*";

    @Context HttpHeaders httpHeaders;

    @Override
    public Response toResponse(DefaultOptionsMethodException exception) {

        final ResponseBuilder response = Response.ok();

        String requestHeaders = httpHeaders.getHeaderString(ACCESS_CONTROL_REQUEST_HEADERS);
        String requestMethods = httpHeaders.getHeaderString(ACCESS_CONTROL_REQUEST_METHOD);

        if (requestHeaders != null)
            response.header(ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);

        if (requestMethods != null)
            response.header(ACCESS_CONTROL_ALLOW_METHODS, requestMethods);

        // TODO: development only, too permissive
        response.header(ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN_ANYONE);

        return response.build();
    }
}
Share:
17,005
Eduardo Vendruscolo
Author by

Eduardo Vendruscolo

Updated on June 08, 2022

Comments

  • Eduardo Vendruscolo
    Eduardo Vendruscolo almost 2 years

    I'm doing a REST application. I have made the GET method without issues, however, when I implement the POST method, it says that I don't have the OPTIONS method implemented for it. I am doing the OPTIONS method for URI:

    http://192.168.1.26:8080/sellAppWeb/api/object/

    I have the POST and OPTIONS methods:

    @OPTIONS
    @Produces("application/json; charset=UTF-8")
    public Response options() {
        return Response.ok().build();
    }
    
    @Override
    @POST
    public Response save(CervejaDTO cervejaDTO) {
        cervejaController.register(cervejaDTO);
        return Response.ok(cervejaDTO).build();
    }
    

    Then I am made the DELETE method and again it says that I don't have a OPTIONS method. Then I need to make another OPTIONS method, which has an ID in the URI end. For example to delete a object with id = 3:

    http://192.168.1.26:8080/sellAppWeb/api/object/3

    I need to have another OPTIONS with same structure of DELETE URI:

    @OPTIONS
    @Path("/{id}")
    @Produces("application/json; charset=UTF-8")
    public Response optionsDelete(@PathParam("id") Integer id) {
        return Response.ok().build();
    }
    
    @Override
    @POST
    public Response save(CervejaDTO cervejaDTO) {
        cervejaController.register(cervejaDTO);
        return Response.ok(cervejaDTO).build();
    }
    

    Does anyone have a way to do a generic OPTIONS for all REST requests?

    the web.xml:

    <display-name>Testes de serviços REST</display-name>
    <description>Testes de serviços REST</description>
    
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
    
    <context-param>
        <param-name>resteasy.scan</param-name>
        <param-value>true</param-value>
    </context-param>
    
    <context-param>
        <param-name>resteasy.servlet.mapping.prefix</param-name>
        <param-value>/api</param-value>
    </context-param>
    
    <context-param>
        <param-name>resteasy.providers</param-name>
        <param-value>br.com.sell.app.exception.handler.DefaultExceptionHandler</param-value>
    </context-param>
    
    <listener>
        <listener-class>org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap</listener-class>
    </listener>
    
    <servlet>
        <servlet-name>resteasy-servlet</servlet-name>
        <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>resteasy-servlet</servlet-name>
        <url-pattern>/api/*</url-pattern>
    </servlet-mapping>
    

  • Buhake Sindi
    Buhake Sindi over 8 years
    Why do you recommend using Spring when Java EE has JAX-RS in the specification? JBoss Wildlfy has JAX-RS support in the box and doesn't use any Spring REST or Spring Data under the covers. In fact, one can write an entire RESTful application without using Spring and purely JAX-RS.
  • Eduardo Vendruscolo
    Eduardo Vendruscolo over 8 years
    I modify my web.xml like your examle, but the server don't starts. And the @Consumes annotation, dont solve the problem too.
  • Eduardo Vendruscolo
    Eduardo Vendruscolo over 8 years
    org.jboss.resteasy.spi.DefaultOptionsMethodException: No resource method found for options, return OK with Allow header
  • Eduardo Vendruscolo
    Eduardo Vendruscolo over 8 years
    I have a CORS filter, it's implemented like bellow: headers.add("Access-Control-Allow-Origin", "*"); headers.add("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS"); headers.add("Access-Control-Allow-Headers", "Content-Type");
  • Paul Samsotha
    Paul Samsotha over 8 years
    Add a print statement in the filter's constructor to make sure it's created. Second make a simple GET request, and look at the headers to make sure they are there. If you add the headers to all request (as you are in your filter), the headers should even show for GET requests. The nice thing about RESTesy's filter is that it is implemented to only send the response headers for preflight (OPTIONS) request, so the headers aren't sent for all requests. It's not a problem if the headers are set for all responses, it just makes the response a little lighter.
  • Paul Samsotha
    Paul Samsotha over 8 years
    You may also want to just try and use RESTeasy's filter instead of your own.
  • Buhake Sindi
    Buhake Sindi over 8 years
    I see you're using RESTEasy specify configuration. Can you post the stacktrace as well so that we can see what happens? Also, what do you get when you access the link http://192.168.1.26:8080/sellAppWeb/api/?
  • Eduardo Vendruscolo
    Eduardo Vendruscolo over 8 years
    i get this error, but i this it's correct. because i dont have path mapped to this address <response> <message> Could not find resource for full path: http://192.168.1.103:8080/sellAppWeb/api/ </message> </response>
  • Eduardo Vendruscolo
    Eduardo Vendruscolo over 8 years
    Yes, the constructor print's in the console when created, the response of a simple GET Request, is Access-Control-Allow-Headers:Content-Type Access-Control-Allow-Methods:GET,POST,DELETE,PUT,OPTIONS Access-Control-Allow-Origin:* Connection:keep-alive Content-Length:994 Content-Type:application/json;charset=UTF-8 Date:Thu, 03 Sep 2015 18:01:11 GMT Server:WildFly/9 X-Powered-By:Undertow/1
  • Paul Samsotha
    Paul Samsotha over 8 years
    Please try and use the RESTeasy filter
  • Eduardo Vendruscolo
    Eduardo Vendruscolo over 8 years
    which class i need to implement, to use the RESTEasy filter? the ContainerResponseFilter class it's default's java EE class from javax.ws.rs.container package.
  • Paul Samsotha
    Paul Samsotha over 8 years
    You don't need to implement anything. I meant register resteasys CorsFilter. Look at my answer