Enabling CORS in Akka HTTP

If you're hosting your front-end site on x.com and the backend on api.x.com (or any other domain), you'll need to configure the backend server to send a bunch of headers telling the browser its okay for x.com's javascript to make remote requests to api.x.com.

Doing so is not entirely easy with Akka HTTP.

Speaking of which, pretty much anything to do with Akka HTTP's routing directives is not easy. Yes, its powerful, but its also ugly to read - and hard to make changes to. Anyway, here's how I managed to enable CORS, which may not be the best way, but it will at least work and not cause you to tear your hair out.

In a nutshell:

1) With your complete() statements, you'll need to send in the additional headers.

2) You'll need to add an OPTIONS route handler for all (most?) of your endpoints - at least for the POST ones. Browsers (at least chrome) will hit the OPTIONS for your endpoints before sending POST requests - to verify the CORS. If you don't have an OPTIONS endpoint set up - you will go through PAIN and MISERY like I did, until you figure it out.

1) Add a helper class for setting the headers

Here's the one I used - courtesy of https://dzone.com/articles/handling-cors-in-akka-http :


import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.model.{HttpResponse, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.directives.RouteDirectives.complete
import akka.http.scaladsl.server.{Directive0, Route}

import scala.concurrent.duration._


/**
  * From https://dzone.com/articles/handling-cors-in-akka-http
  * and https://ali.actor
  */
trait CORSHandler {

  private val corsResponseHeaders = List(
    `Access-Control-Allow-Origin`.*,
    `Access-Control-Allow-Credentials`(true),
    `Access-Control-Allow-Headers`("Authorization",
      "Content-Type", "X-Requested-With"),
    `Access-Control-Max-Age`(1.day.toMillis) //Tell browser to cache OPTIONS requests
  )


  //this directive adds access control headers to normal responses
  private def addAccessControlHeaders: Directive0 = {
    respondWithHeaders(corsResponseHeaders)
  }

  //this handles preflight OPTIONS requests.
  private def preflightRequestHandler: Route = options {
    complete(HttpResponse(StatusCodes.OK).
             withHeaders(`Access-Control-Allow-Methods`(OPTIONS, POST, PUT, GET, DELETE)))
  }

  // Wrap the Route with this method to enable adding of CORS headers
  def corsHandler(r: Route): Route = addAccessControlHeaders {
    preflightRequestHandler ~ r
  }

  // Helper method to add CORS headers to HttpResponse
  // preventing duplication of CORS headers across code
  def addCORSHeaders(response: HttpResponse):HttpResponse =
    response.withHeaders(corsResponseHeaders)
}

Not the prettiest looking, but it will get the job done - and this isn't something you should need to change all that much. corsHandler(route) is the only one you'll be calling.

2) Modify your routes

As mentioned - you'll need to add an OPTIONS route to your endpoints. For your regular endpoints, you'll need to pass in your complete() routes into the CORSHandler class. Here's an example:

private val cors = new CORSHandler {} //Cache this somewhere

//A simple /ping endpoint which will echo 'Pong' for any POST requests

val ping: Route = path("ping") {
    //The OPTIONS endpoint - doesn't need to do anything except send an OK
    options {
      cors.corsHandler(complete(StatusCodes.OK))
    } ~ post( cors.corsHandler(complete("Pong")) ) //Handle the POST
  }


//Pass in the route to your server
val binding = Http().bindAndHandle(ping, "0.0.0.0" , 8000)

And that's it. You'll finally be able to make remote HTTP requests without getting that ugly 405 error.

comments powered by Disqus