user3816466 user3816466 - 8 days ago 6
Scala Question

Can I create a default OPTIONS method directive for all entry points in my route?

I don't want to explicitly write:

options { ... }


for each entry point / path in my Spray route. I'd like to write some generic code that will add
OPTIONS
support for all paths. It should look at the routes and extract supported methods from them.

I can't paste any code since I don't know how to approach it in Spray.

The reason I'm doing it is I want to provide a self discoverable API that adheres to HATEOAS principles.

Answer

The below directive will be able to catch a rejected request, check if it is a option request, and return:

  • The CORS headers, to support CORS (this directive removes ALL cors protection, beware!!!!!)
  • The Allow headers, to give the peer a list of available methods

Try to understand the below snippet and adjust it where necessary. You should prefer to deliver as much information as possible, but if you only want to return the Allowed methods I suggest you cut out the rest :).

import spray.http.{AllOrigins, HttpMethods, HttpMethod, HttpResponse}
import spray.http.HttpHeaders._
import spray.http.HttpMethods._
import spray.routing._

/**
 * A mixin to provide support for providing CORS headers as appropriate
 */
trait CorsSupport {
  this: HttpService =>

  private val allowOriginHeader = `Access-Control-Allow-Origin`(AllOrigins)
  private val optionsCorsHeaders = List(
    `Access-Control-Allow-Headers`(
      "Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Accept-Language, Host, " +
      "Referer, User-Agent"
    ),
    `Access-Control-Max-Age`(60 * 60 * 24 * 20)  // cache pre-flight response for 20 days
  )

  def cors[T]: Directive0 = mapRequestContext {
    context => context.withRouteResponseHandling {
      // If an OPTIONS request was rejected as 405, complete the request by responding with the
      // defined CORS details and the allowed options grabbed from the rejection
      case Rejected(reasons) if (
        context.request.method == HttpMethods.OPTIONS &&
        reasons.exists(_.isInstanceOf[MethodRejection])
      ) => {
        val allowedMethods = reasons.collect { case r: MethodRejection => r.supported }
        context.complete(HttpResponse().withHeaders(
          `Access-Control-Allow-Methods`(OPTIONS, allowedMethods :_*) ::
          allowOriginHeader ::
          optionsCorsHeaders
        ))
      }
    } withHttpResponseHeadersMapped { headers => allowOriginHeader :: headers }
  }
}

Use it like this:

val routes: Route =
  cors {
    path("hello") {
      get {
        complete {
          "GET"
        }
      } ~
      put {
        complete {
          "PUT"
        }
      }
    }
  }

Resource: https://github.com/giftig/mediaman/blob/22b95a807f6e7bb64d695583f4b856588c223fc1/src/main/scala/com/programmingcentre/utils/utils/CorsSupport.scala