-
-
Save joseraya/176821d856b43b1cfe19 to your computer and use it in GitHub Desktop.
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" | |
} | |
} | |
} | |
} |
I've also unit-tested my tweak of this gist with scalatest, in case that's useful to anyone:
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?
@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.
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
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
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
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"}""")
}
}
}
}
}
Can anyone help?
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:
is better expressed as
and
can be combined into a collect like
Thought I'd point those out since they do nice things for readability by ditching some isInstanceOf / asInstanceOf madness.
Thanks for the handy snippet.