Is Play 2.7's WsClient supposed to be stateless?

We’re in the process of upgrading from Play 2.6 to Play 2.7 and have run into a small issue. Some of our functional tests spin up our application and hit the endpoints using WsClient. These tests were all passing in Play 2.6 but some of them fail now we’re on Play 2.7.

After doing some digging it appears that the WsClient in Play 2.7 isn’t stateless and
actually preserves the cookie between requests. II’m not sure if this was an intended feature or a side effect of some other change but I couldn’t see it documented anywhere?

I’ve created a very small sample app which demonstrates this:


GET         /echo-session    wsclient.EchoSessionController.echoSession


package wsclient

import javax.inject.{Inject, Singleton}
import play.api.libs.json.Json
import play.api.mvc._
import play.api.{Logger, LoggerLike}

import scala.concurrent.ExecutionContext

class EchoSessionController @Inject()()
  (cc: ControllerComponents)(implicit executionContext: ExecutionContext) extends AbstractController(cc) {

  protected val logger: LoggerLike = Logger(this.getClass)

  val echoSession: Action[AnyContent] = Action { implicit request: Request[AnyContent] =>
    if (request.session.get("session-exists").isDefined) {
      logger.warn(s"Session exists. [$request] [${request.headers.get("Cookie")}]")
    } else {
      logger.warn(s"Session does not exist. [$request] [${request.headers.get("Cookie")}]")
      Ok(Json.toJson("session-exists" -> "true")


package wsclient

import org.scalatest.concurrent.ScalaFutures
import org.scalatest.time.{Millis, Seconds, Span}
import org.scalatest.{Matchers, WordSpec}

class WsClientISpec extends WordSpec with Matchers with ScalaFutures with GuiceOneServerPerSuite {

  implicit val system: ActorSystem = ActorSystem("system")
  implicit val materializer: ActorMaterializer = ActorMaterializer()

  override implicit val patienceConfig: PatienceConfig =
    PatienceConfig(timeout = Span(10, Seconds), interval = Span(50, Millis))

  "AhcWsClient" should {
    "be stateless if we share an instance" in {
      val wsClient: AhcWSClient = AhcWSClient()

      val result_1 = wsClient.url("http://localhost:" + portNumber.value + "/echo-session").get().futureValue

      result_1.body shouldBe "{}"

      val result_2 = wsClient.url("http://localhost:" + portNumber.value + "/echo-session").get().futureValue

      // Play 2.6 = Pass
      // Play 2.7 = Fail [Actual   :"{["session-exists":"true"]}"] - For some reason the second call uses the cookie which is returned from the 1st call so restores the session.
      result_2.body shouldBe "{}"


  "AhcWsClient" should {
    "be stateless if we create a fresh instance each time" in {
      val wsClient_1: AhcWSClient = AhcWSClient()
      val wsClient_2: AhcWSClient = AhcWSClient()

      val result_1 = wsClient_1.url("http://localhost:" + portNumber.value + "/echo-session").get().futureValue

      result_1.body shouldBe "{}"

      val result_2 = wsClient_2.url("http://localhost:" + portNumber.value + "/echo-session").get().futureValue

      // Play 2.6 = Pass
      // Play 2.7 = Pass
      result_2.body shouldBe "{}"


I had the exact same issue. I simply swapped WSClient for HttpURLConnection in my test.

It should not send the cookies unless you ask for it for that specific request.

Can you create a full project that reproduces the problem?


Sure thing -

Hey @HerbiePorter,

This fixes the problem:

It will be part of the next release of play-ws.

1 Like