Todd Flanders
Todd Flanders

Reputation: 383

Can I define multiple HTTP Verbs for an action in Play Framework 2?

I have an app that requires I define an action for both the HEAD and GET verbs. Another app (Mozilla Open Badges) calls my app using two HTTP requests. It first uses HEAD to verify that my URL looks like it returns the correct type of response, then uses GET against the same URL to fetch the contents. The following approach works:

GET   /assertion                        controllers.Assertion.index
HEAD  /assertion                        controllers.Assertion.index

...this works, but is a DRY violation (Don't Repeat Yourself).

I'd prefer something like:

(GET, HEAD)   /assertion                        controllers.Assertion.index

...but this is not allowed. I would also be happy if GET gave you HEAD for free, but there may be reasons that I don't understand to block HEAD for a GET action.

I suppose the redundant Path-to-Action definition isn't the end of the world, but I like to keep my code clean where possible.

Based on the W3C spec for HEAD (http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html), I belive Play Framework 2 is behaving incorrecly for not allowing a HEAD call on a route defined for GET:

9.4 HEAD

The HEAD method is identical to GET except that the server MUST NOT return a 
message-body in the response. The metainformation contained in the HTTP headers in
response to a HEAD request SHOULD be identical to the information sent in response 
to a GET request. This method can be used for obtaining metainformation about the
entity implied by the request without transferring the entity-body itself. This 
method is often used for testing hypertext links for validity, accessibility, and 
recent modification.

Upvotes: 3

Views: 1301

Answers (1)

biesior
biesior

Reputation: 55798

In Play 2.0+ there's not possible to use wildcard VERBs (like * or ANY or multivalued) (which could be a solution for you), so in this case 'DRY violation' you are using is only official way.

Edit

Actually you can try another approach:

  • on the end of routes file put 'catch-all HEAD` rulez
  • If there is not specified route for HEAD /path 'catch-all' sends it to autoHead(path: String) action.
  • There you can forward all headers from request to GET version of the route with WebServices, then get the response and return headers only.

I added working sample of this approach (Java only) to play-simple-rest sample app. The things important for redirecting are:

public static Result autoHead(String originalPath) throws IllegalAccessException {

    WS.WSRequestHolder forwardedRequest = WS.url("http://" + request().host() + request().path());
    // this header will allow you to make additional choice i.e. avoid tracking the request or something else
    // see condition in index() action
    forwardedRequest.setHeader("X_FORWARD_FROM_HEAD", "true");

    // Forward original headers
    for (String header : request().headers().keySet()) {
        forwardedRequest.setHeader(header, request().getHeader(header));
    }

    // Forward original queryString
    for (String key : request().queryString().keySet()) {
        for (String val : request().queryString().get(key)) {
            forwardedRequest.setQueryParameter(key, val);
        }
    }

    // Call the same path but with GET
    WS.Response wsResponse = forwardedRequest.get().get();

    // Set returned headers to the response
    for (Field f : Http.HeaderNames.class.getFields()) {
        String headerName = f.get(null).toString();
        if (wsResponse.getHeader(headerName) != null) {
            response().setHeader(headerName, wsResponse.getHeader(headerName));
        }
    }

    return status(wsResponse.getStatus());
}
public static boolean forwardedFromHead() {
    return (request().getHeader("X_FORWARD_FROM_HEAD") != null && "true".equals(request().getHeader("X_FORWARD_FROM_HEAD")));
}
  • And two routes HEAD on the end of the file route file

    HEAD    /               controllers.Application.autoHead(originalPath:String ?= "/")
    HEAD    /*originalPath  controllers.Application.autoHead(originalPath:String)
    

P.S. If you would like to write something similar in Scala it would be nice :)

Upvotes: 2

Related Questions