5

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.

3 Answers3

2

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

RoyB
  • 3,104
  • 1
  • 16
  • 37
-1

I did it like this:

private val CORSHeaders = List(
  `Access-Control-Allow-Methods`(GET, POST, PUT, DELETE, OPTIONS),
  `Access-Control-Allow-Headers`("Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Accept-Language, Host, Referer, User-Agent"),
  `Access-Control-Allow-Credentials`(true)
)

def respondWithCORS(origin: String)(routes: => Route) = {
  val originHeader = `Access-Control-Allow-Origin`(SomeOrigins(Seq(HttpOrigin(origin))))

  respondWithHeaders(originHeader :: CORSHeaders) {
    routes ~ options { complete(StatusCodes.OK) }
  }
}

val routes =
  respondWithCORS(config.getString("origin.domain")) {
    pathPrefix("api") {
      // ... your routes here
    }
  }

So every OPTION request to any URL with /api prefix returns 200 code.

Update: added Access* headers.

sap1ens
  • 2,877
  • 1
  • 27
  • 30
  • If you added minus - please explain. This solution works perfectly in production for many projects. – sap1ens Aug 12 '15 at 13:49
  • You didn't answer the question that was asked, he asks for a response containing the ALLOW keyword. This will be a more usefull response as the peer will know what HTTP methods will be available. Also, if you want to support CORS you need to supply the CORS headers with the response. – RoyB Nov 28 '16 at 13:37
  • @RoyB there is not a word about CORS or Access* headers in the question. Not a single one. But of course, we can assume they're needed, so I updated my answer. – sap1ens Nov 30 '16 at 03:54
  • 1
    No CORS indeed, but Allow headers for showing what methods are allowed. I quote: `It should look at the routes and extract supported methods from them.` Your provided solution returns all methods that exist, and not the methods that specifically exist for the current endpoint, so your solution is still not advisable, and will cause browsers to hit methods on endpoints they wouldn't normally be allowed to hit or ones that might not even exist. – RoyB Nov 30 '16 at 08:55
-2

Methinks options is generic enough, you can use it as:

path("foo") {
  options {
    ...
  }
} ~
path("bar") {
  options {
    ...
  }
}

or as this:

options {
  path("foo") {
    ...
  } ~
  path("bar") {
    ...
  }
}
vitalii
  • 3,335
  • 14
  • 18
  • It compiles like this: `options { get { ... } }` but does not work as expected - only `OPTIONS` method is available and I can't call internal route with `GET`. Does it work for you, maybe I'm missing something? – user3816466 Aug 21 '14 at 15:37
  • I mean I'm trying to avoid writing `path("foo") { options {...} ~ get {...} }`. – user3816466 Aug 21 '14 at 15:39
  • yes it works for me in any combination. You can create custom directive in spray, see http://spray.io/documentation/1.1-SNAPSHOT/spray-routing/advanced-topics/custom-directives/ but it is a bit advanced, as type signatures could be pretty involved. btw maybe you want something like this: val getOrPut = get | put ? – vitalii Aug 26 '14 at 14:09