Cached recalculates etag even if content did not change · Issue #13783 · playframework/playframework · GitHub
Skip to content

Cached recalculates etag even if content did not change #13783

@OlegYch

Description

@OlegYch

here is a test with desired behavior for play 3.0.10

package play.api.cache

import java.time.Instant
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent.duration.*
import scala.util.Random
import jakarta.inject.*
import play.api.cache.ehcache.EhCacheApi
import play.api.http
import play.api.mvc.*
import play.api.test.*
import play.api.Application

import scala.concurrent.Future

class CachedSpec2 extends PlaySpecification {
  sequential

  def cached(implicit app: Application) = {
    new Cached(app.injector.instanceOf[AsyncCacheApi])(using app.materializer)
  }

  // Tests here don't use the body
  val Action = ActionBuilder.ignoringBody

  "the cached action" should {
    "refresh only if content changed" in new WithApplication() {
      override def running() = {
        var content = "1"
        val action  = cached(using app)(_.uri, duration = 1)(Action(Results.Ok(content)))
        val resultA = action(FakeRequest("GET", "/a")).run()
        status(resultA) must_== 200
        contentAsString(resultA) must_== content

        def useOldEtag(from: Future[Result] = resultA) =
          action(FakeRequest("GET", "/a").withHeaders(IF_NONE_MATCH -> header(ETAG, from).get)).run()

        status(useOldEtag()) must_== NOT_MODIFIED
        Thread.sleep(1500) // sleep to expire cache
        val resultAA = useOldEtag() // old etag refreshes cache
        status(resultAA) must_== 200 // same content is returned on first request
        header(ETAG, resultAA).get must_== header(ETAG, resultA).get
        header(EXPIRES, resultAA).get must_!= header(EXPIRES, resultA).get // expires is updated
        val resultAAA = useOldEtag() // subsequent requests have same etag and expires
        status(resultAAA) must_== NOT_MODIFIED
        header(ETAG, resultAA).get must_== header(ETAG, resultAAA).get
        header(EXPIRES, resultAA).get must_== header(EXPIRES, resultAAA).get
        // but updating content invalidates the old etag
        content = "2"
        Thread.sleep(1500) // sleep to expire cache
        val resultB = useOldEtag()
        status(resultB) must_== 200
        contentAsString(resultB) must_== content
        status(useOldEtag()) must_== 200
        status(useOldEtag(from = resultB)) must_== NOT_MODIFIED
      }
    }
  }
}

this causes sending a complete response to all clients every time cache entry expires, even if it did not change

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions