Globally handling OPTIONS requests in Play Framework

If you’re using AJAX to talk to a Play Framework application, you’ll probably need to respond to OPTIONS requests and might need to return the correct access control (CORS) headers.

In a controller, we can easily define a handler to accept OPTIONS requests:

def headers = List(
  "Access-Control-Allow-Origin" -> "*",
  "Access-Control-Allow-Methods" -> "GET, POST, OPTIONS, DELETE, PUT",
  "Access-Control-Max-Age" -> "3600",
  "Access-Control-Allow-Headers" -> "Origin, Content-Type, Accept, Authorization",
  "Access-Control-Allow-Credentials" -> "true"
)

def options = Action { request =>
  NoContent.withHeaders(headers : _*)
}

And we can call our new options handler from our routes file, but this has a few problems. We either need to implement an options handler for every route, or we send the same response whatever route we have, even if it doesn’t exist.

Per-route

If you want to respond on a per-route basis, that typically requires one additional line in your routes file for every route you define:

GET / controllers.Application.index
OPTIONS / controllers.Application.options

Globally

Or, if you don’t mind sending the same headers back for every OPTIONS request (even if the route doesn’t really exist), there’s a cheat:

OPTIONS / controllers.Application.rootOptions
OPTIONS /*url controllers.Application.options(url: String)

and change your controller options handler to:

def rootOptions = options("/")
def options(url: String) = Action { request =>
    NoContent.withHeaders(headers : _*)
}

You can still override the global OPTIONS per-route by adding additional routes before the wildcard, for example:

OPTIONS /foo controllers.Application.someCustomOptions
OPTIONS / controllers.Application.rootOptions
OPTIONS /*url controllers.Application.options(url: String)

Or we can abuse Play Framework - the best way!

Play Framework doesn’t like to expose its routing, making it difficult to inspect the routing table once its been created. But it is possible! Doing that, we can globally handle OPTIONS requests but dynamically respond based on URL (or even other request parameters).

For this example, we’ll work out the Allow header so we can return a 204 response if the route would normally exist, but a 404 response if it wouldn’t.

This is the example routes file:

GET /           controllers.Application.index
GET /foo        controllers.Application.foo
OPTIONS /       controllers.Application.rootOptions
OPTIONS /*url   controllers.Application.options(url: String)

When sending OPTIONS requests, we want to respond with 204 and Allow: GET, OPTIONS for / and /foo, but respond with 404 for everything else.

Getting the methods available for a URL

Play Framework gives us a convenient function - handlerFor - which is normally used to route requests to a handler. For this to work, you’ll need to add an import:

import play.api.Play.current

We can then define a getMethods function, which given a request will return a list of available methods. It does this by asking Play Framework to route new requests with modified method parameters. If a handler is found, the method is added to the list. The list is also cached for future requests.

val methodList = List("GET", "POST", "PUT", "DELETE", "PATCH")
def getMethods(request: Request[AnyContent]) : List[String] = {
    Cache.getOrElse[List[String]]("options.url." + request.uri) {
        for(m <- methodList; if Play.application.routes.get.handlerFor(new RequestHeader {
            val remoteAddress = request.remoteAddress
            val headers = request.headers
            val queryString = request.queryString
            val version = request.version
            val method = m
            val path = request.path
            val uri = request.uri
            val tags = request.tags
            val id: Long = request.id
        }).isDefined) yield m
    }
}

We can then update our options action to use the new method list:

def options(url: String) = Action { request =>
    val methods = List("OPTIONS") ++ getMethods(request)
    if(methods.length > 1)
        NoContent.withHeaders(List("Allow" -> methods.mkString(", ")) : _*)
    else
        NotFound
}

We add OPTIONS back in, and if we have more than one method we return the Allow header, otherwise a 404 response.

We could instead cache the entire response for a given URI, but caching just the method list gives us the flexibility to set other headers which may be more dynamic, for example Last-Modified. Even the current caching might be too restrictive if the available methods depends on other request parameters.