Http4s provides Middleware, named CORS, for adding the appropriate headers to responses to allow Cross Origin Resource Sharing.

Examples in this document have the following dependencies.

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-dsl" % http4sVersion,
  "org.http4s" %% "http4s-server" % http4sVersion
)

And we need some imports.

import org.http4s._
import org.http4s.dsl._

Let’s start by making a simple service.

val service = HttpService {
  case _ =>
    Ok()
}
// service: org.http4s.HttpService = Kleisli(org.http4s.package$HttpService$$$Lambda$25160/573865256@9ba371e)

val request = Request(Method.GET, uri("/"))
// request: org.http4s.Request = Request(method=GET, uri=/, headers=Headers())

service(request).run
// <console>:21: warning: method run in class Task is deprecated (since 7.2): use unsafePerformSync
//        service(request).run
//                         ^
// res0: org.http4s.Response = Response(status=200, headers=Headers())

Now we can wrap the service in the CORS middleware.

import org.http4s.server.middleware._
// import org.http4s.server.middleware._

val corsService = CORS(service)
// corsService: org.http4s.HttpService = Kleisli(org.http4s.server.middleware.CORS$$$Lambda$25200/710092457@571b0fc2)

corsService(request).run
// <console>:24: warning: method run in class Task is deprecated (since 7.2): use unsafePerformSync
//        corsService(request).run
//                             ^
// res1: org.http4s.Response = Response(status=200, headers=Headers())

So far, there was no change. That’s because an Origin header is required in the requests. This, of course, is the responsibility of the caller.

val originHeader = Header("Origin", "somewhere.com")
// originHeader: org.http4s.Header.Raw = Origin: somewhere.com

val corsRequest = request.putHeaders(originHeader)
// corsRequest: request.Self = Request(method=GET, uri=/, headers=Headers(Origin: somewhere.com))

corsService(corsRequest).run
// <console>:24: warning: method run in class Task is deprecated (since 7.2): use unsafePerformSync
//        corsService(corsRequest).run
//                                 ^
// res2: org.http4s.Response = Response(status=200, headers=Headers(Access-Control-Allow-Headers: Content-Type, *, Vary: Origin,Access-Control-Request-Methods, Access-Control-Allow-Credentials: true, Access-Control-Allow-Methods: GET, Access-Control-Allow-Origin: somewhere.com, Access-Control-Max-Age: 86400))

Notice how the response has the CORS headers added. How easy was that? And, as described in Middleware, services and middleware can be composed such that only some of your endpoints are CORS enabled.

Configuration

The example above showed the default configuration for CORS, which adds the headers to any successul response, regardless of origin or HTTP method. There are configuration options to modify that.

First, we’ll create some requests to use in our example. We want these requests have a variety of origins and methods.

val googleGet = Request(Method.GET, uri("/"), headers = Headers(Header("Origin", "google.com")))
// googleGet: org.http4s.Request = Request(method=GET, uri=/, headers=Headers(Origin: google.com))

val yahooPut = Request(Method.PUT, uri("/"), headers = Headers(Header("Origin", "yahoo.com")))
// yahooPut: org.http4s.Request = Request(method=PUT, uri=/, headers=Headers(Origin: yahoo.com))

val duckPost = Request(Method.POST, uri("/"), headers = Headers(Header("Origin", "duckduckgo.com")))
// duckPost: org.http4s.Request = Request(method=POST, uri=/, headers=Headers(Origin: duckduckgo.com))

Now, we’ll create a configuration that limits the allowed methods to GET and POST, pass that to the CORS middleware, and try it out on our requests.

import scala.concurrent.duration._
// import scala.concurrent.duration._

val methodConfig = CORSConfig(
  anyOrigin = true,
  anyMethod = false,
  allowedMethods = Some(Set("GET", "POST")),
  allowCredentials = true,
  maxAge = 1.day.toSeconds)
// methodConfig: org.http4s.server.middleware.CORSConfig = CORSConfig(true,true,86400,false,None,Some(Set(GET, POST)),Some(Set(Content-Type, *)))

val corsMethodSvc = CORS(service, methodConfig)
// corsMethodSvc: org.http4s.HttpService = Kleisli(org.http4s.server.middleware.CORS$$$Lambda$25200/710092457@121a808b)

corsMethodSvc(googleGet).run
// <console>:27: warning: method run in class Task is deprecated (since 7.2): use unsafePerformSync
//        corsMethodSvc(googleGet).run
//                                 ^
// res3: org.http4s.Response = Response(status=200, headers=Headers(Access-Control-Allow-Headers: Content-Type, *, Vary: Origin,Access-Control-Request-Methods, Access-Control-Allow-Credentials: true, Access-Control-Allow-Methods: GET, POST, Access-Control-Allow-Origin: google.com, Access-Control-Max-Age: 86400))

corsMethodSvc(yahooPut).run
// <console>:27: warning: method run in class Task is deprecated (since 7.2): use unsafePerformSync
//        corsMethodSvc(yahooPut).run
//                                ^
// res4: org.http4s.Response = Response(status=200, headers=Headers())

corsMethodSvc(duckPost).run
// <console>:27: warning: method run in class Task is deprecated (since 7.2): use unsafePerformSync
//        corsMethodSvc(duckPost).run
//                                ^
// res5: org.http4s.Response = Response(status=200, headers=Headers(Access-Control-Allow-Headers: Content-Type, *, Vary: Origin,Access-Control-Request-Methods, Access-Control-Allow-Credentials: true, Access-Control-Allow-Methods: GET, POST, Access-Control-Allow-Origin: duckduckgo.com, Access-Control-Max-Age: 86400))

As you can see, the CORS headers were only added to the GET and POST requests. Next, we’ll create a configuration that limits the origins to “yahoo.com” and “duckduckgo.com”.

val originConfig = CORSConfig(
  anyOrigin = false,
  allowedOrigins = Some(Set("yahoo.com", "duckduckgo.com")),
  allowCredentials = false,
  maxAge = 1.day.toSeconds)
// originConfig: org.http4s.server.middleware.CORSConfig = CORSConfig(false,false,86400,true,Some(Set(yahoo.com, duckduckgo.com)),None,Some(Set(Content-Type, *)))

val corsOriginSvc = CORS(service, originConfig)
// corsOriginSvc: org.http4s.HttpService = Kleisli(org.http4s.server.middleware.CORS$$$Lambda$25200/710092457@2d89023d)

corsOriginSvc(googleGet).run
// <console>:27: warning: method run in class Task is deprecated (since 7.2): use unsafePerformSync
//        corsOriginSvc(googleGet).run
//                                 ^
// res6: org.http4s.Response = Response(status=200, headers=Headers())

corsOriginSvc(yahooPut).run
// <console>:27: warning: method run in class Task is deprecated (since 7.2): use unsafePerformSync
//        corsOriginSvc(yahooPut).run
//                                ^
// res7: org.http4s.Response = Response(status=200, headers=Headers(Access-Control-Allow-Headers: Content-Type, *, Vary: Origin,Access-Control-Request-Methods, Access-Control-Allow-Credentials: false, Access-Control-Allow-Methods: PUT, Access-Control-Allow-Origin: yahoo.com, Access-Control-Max-Age: 86400))

corsOriginSvc(duckPost).run
// <console>:27: warning: method run in class Task is deprecated (since 7.2): use unsafePerformSync
//        corsOriginSvc(duckPost).run
//                                ^
// res8: org.http4s.Response = Response(status=200, headers=Headers(Access-Control-Allow-Headers: Content-Type, *, Vary: Origin,Access-Control-Request-Methods, Access-Control-Allow-Credentials: false, Access-Control-Allow-Methods: POST, Access-Control-Allow-Origin: duckduckgo.com, Access-Control-Max-Age: 86400))

Again, the results are as expected. You can, of course, create a configuration that combines limits on both HTTP method and origin.

As described in Middleware, services and middleware can be composed such that only some of your endpoints are CORS enabled.