Skip to content

Instantly share code, notes, and snippets.

@joseraya
Created July 1, 2014 21:24
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save joseraya/176821d856b43b1cfe19 to your computer and use it in GitHub Desktop.
Save joseraya/176821d856b43b1cfe19 to your computer and use it in GitHub Desktop.
CORS directive for Spray
package com.agilogy.spray.cors
import spray.http.{HttpMethods, HttpMethod, HttpResponse, AllOrigins}
import spray.http.HttpHeaders._
import spray.http.HttpMethods._
import spray.routing._
// see also https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
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`(1728000))
def cors[T]: Directive0 = mapRequestContext { ctx => ctx.withRouteResponseHandling({
//It is an option requeset for a resource that responds to some other method
case Rejected(x) if (ctx.request.method.equals(HttpMethods.OPTIONS) && !x.filter(_.isInstanceOf[MethodRejection]).isEmpty) => {
val allowedMethods: List[HttpMethod] = x.filter(_.isInstanceOf[MethodRejection]).map(rejection=> {
rejection.asInstanceOf[MethodRejection].supported
})
ctx.complete(HttpResponse().withHeaders(
`Access-Control-Allow-Methods`(OPTIONS, allowedMethods :_*) :: allowOriginHeader ::
optionsCorsHeaders
))
}
}).withHttpResponseHeadersMapped { headers =>
allowOriginHeader :: headers
}
}
}
val routes: Route =
cors {
path("hello") {
get {
complete {
"GET"
}
} ~
put {
complete {
"PUT"
}
}
}
}
@giftig
Copy link

giftig commented Jan 4, 2015

Thanks, this is handy! I'm adapting this a bit and putting it into my project now. I did make a couple of amendments to make better use of Scala's List methods, though:

!x.filter(_.isInstanceOf[MethodRejection]).isEmpty)

is better expressed as

x.exists(_.isInstanceOf[MethodRejection])

and

x.filter(_.isInstanceOf[MethodRejection]).map(rejection=> {
rejection.asInstanceOf[MethodRejection].supported
})

can be combined into a collect like

x.collect { case rejection: MethodRejection => rejection.supported }

Thought I'd point those out since they do nice things for readability by ditching some isInstanceOf / asInstanceOf madness.

Thanks for the handy snippet.

@giftig
Copy link

giftig commented Jan 4, 2015

@tuler
Copy link

tuler commented Jan 10, 2015

I can't make this work with spray 1.3.2. Should it work?
The case Rejected is not being called. Spray is responding 405, and with header 'Allow' instead of 'Access-Control-Allow-Methods'.
Did this kind of handling change in the latest version of spray?

@waymost
Copy link

waymost commented Mar 9, 2015

@tuler I was able to successfully integrate this with spray 1.3.2. Make sure you include any request headers you use in Access-Control-Allow-Headers. I got the same thing with my Authorization header until I included it there.

Also, it's worth noting that if you get a request timeout, the browser will give you a CORS error for the request as the timeout response does not contain allowOriginHeader. This is because timeouts in spray have their own route that generates its own HttpResponse. To resolve this, you need to override timeoutRoute in the trait.

I've forked this gist and applied this and @giftig's fixes here.

@hectorgool
Copy link

I have the following problem:
To compile the project shows me this:

WidgetService.scala:21: illegal inheritance;
[error] self-type api.WidgetService does not conform to lib.CORSSupport's selftype lib.CORSSupport with spray.routing.HttpService
[error] actorRefFactory: ActorRefFactory) extends RoutedEndpoints with WidgetHandler with UtilBijections with CORSSupport{

The solution may be this:
https://www.safaribooksonline.com/library/view/scala-cookbook/9781449340292/ch08s07.html

I do not get it to work:
What options do I have to fix it? Thanks

my code is here:
https://gist.github.com/hectorgool/df2acef9e82f54de1822

@oreganrob
Copy link

Hi,

I've been trying to get the CORSSupport class to work with Basic Authentication but I'm not getting anywhere fast.
I've added Authorization to the list of supported classes but with my implementation the CORSSupport class doesn't seem to get called at all. It seems almost like the Authorization response is handled separately and never executes the CORS header class as the result I always get back is a 401 with the message that no origin header was present in the response.
Does anyone have any pointers on how to use CORS with Basic Auth?

My route looks like follows...

pathPrefix("v1") {
cors {
pathPrefix("test") {
authenticate(BasicAuth(ClientAuthentication, "API Authentication")) {
customerProfile =>
get {
respondWithMediaType(application/json){
complete("""{"result": "test!"}""")
}
}
}
}
}

Thanks

@oreganrob
Copy link

Ignore above comment. Got it working by adding the following to the CORSSupport class...

|| (ctx.request.method.equals(HttpMethods.OPTIONS)
&& x.exists(_.isInstanceOf[AuthenticationFailedRejection]))

as well as adding "Authorization" to the list of headers

Thanks

@OElesin
Copy link

OElesin commented Sep 27, 2015

This is my implementation:

class MyServiceActor extends Actor with MyService {

def actorRefFactory = context
def requestMethodAndResponseStatusAsInfo(req: HttpRequest): Any => Option[LogEntry] = {
case res: HttpResponse => Some(LogEntry(req.method + ":" + req.uri + ":" + res.message.status, InfoLevel))
case _ => None // other kind of responses
}

def routeWithLogging = logRequestResponse(requestMethodAndResponseStatusAsInfo _)(myRoute ~ testRoute)
def receive = runRoute(routeWithLogging)

}

// this trait defines our service behavior independently from the service actor
trait MyService extends HttpService {

implicit val rejectHandler = RejectionHandler {
case MissingQueryParamRejection(paramName) :: _ => ctx
=> ctx.complete(APIresponse.errorResponse(s"Parameter ' $paramName ' is missing", 404))
case MissingFormFieldRejection(paramName) :: _ => ctx
=> ctx.complete(APIresponse.errorResponse(s"Parameter ' $paramName ' is missing", 404))
}

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(1728000))

def cors[T]: Directive0 = mapRequestContext { ctx => ctx.withRouteResponseHandling({
  //It is an option requeset for a resource that responds to some other method
  case Rejected(x) if (ctx.request.method.equals(HttpMethods.OPTIONS) && !x.filter(_.isInstanceOf[MethodRejection]).isEmpty) => {
    val allowedMethods: List[HttpMethod] = x.filter(_.isInstanceOf[MethodRejection]).map(rejection=> {
      rejection.asInstanceOf[MethodRejection].supported
    })
    ctx.complete(HttpResponse().withHeaders(
      `Access-Control-Allow-Methods`(OPTIONS, allowedMethods :_*) ::  allowOriginHeader ::
       optionsCorsHeaders
    ))
  }
}).withHttpResponseHeadersMapped { headers =>
  allowOriginHeader :: headers

}

}

val userLogic = new UserMgmtLOGIC
var response = new String

val myRoute: Route =
path("api" / "user-service") {
get {
ctx => ctx.complete(APIresponse.successResponese(null, "Welcome to User Management Service"))
}
} ~ cors {
path("api" / "user-service" / "create-user") {
post {
formFields('email, 'user_name, 'password, 'role?) {(email, user_name, password, role) =>
if(email.isEmpty()){
ctx => ctx.complete(APIresponse.errorResponse("Email field is empty"))
}else if(password.isEmpty()){
ctx => ctx.complete(APIresponse.errorResponse("Password field is empty"))
}else if(user_name.isEmpty()){
ctx => ctx.complete(APIresponse.errorResponse("User Name field is empty"))
}else{
val userData = Map("email" -> email, "user_name" -> user_name, "password" -> password, "role" -> role)
var payload = userData.asInstanceOf[Map[String, Any]]
response = userLogic.createUserRecord(payload)
ctx => ctx.complete(response)
}
}
}
}
} ~ path("api" / "user-service" / "delete-user") {
get {
parameter('email){(email) =>
response = userLogic.deleteUserRecord(email)
ctx => ctx.complete(response)
}
}
} ~ path("api" / "user-service" / "all-users") {
get {
response = userLogic.getAllUsers()
ctx => ctx.complete(response)
}
} ~
cors {
path("api" / "user-service" / "login") {
post {
formFields('username, 'password) {(username, password) =>
if(username.isEmpty()){
ctx => ctx.complete(APIresponse.errorResponse("Email field is empty"))
}else if(password.isEmpty()){
ctx => ctx.complete(APIresponse.errorResponse("Password field is empty"))
}else{
response = userLogic.userLogin(username, password)
ctx => ctx.complete(response)
}
}
}
}
} ~ path("api" / "user-service" / "test") {
get {
respondWithMediaType(application/json) {
respondWithHeader(RawHeader("Access-Control-Allow-Origin","*")) {
ctx => ctx.complete("""{"name" : "olalekan"}""")
}
}
}
}
}

And I keep getting this
screen shot 2015-09-27 at 11 50 15 am

Can anyone help?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment