From 7afb0540153ba6b434d7ea67135dd48ee8ed70ad Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:00:21 +0200 Subject: [PATCH 1/5] Add vary header for fetch operations --- .../bluebrain/nexus/delta/routes/ResourcesRoutes.scala | 7 ++++--- .../nexus/delta/routes/ResourcesRoutesSpec.scala | 10 +++++++++- .../nexus/delta/sdk/directives/DeltaDirectives.scala | 9 ++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala index d535808d8e..f1870270a1 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala @@ -144,7 +144,7 @@ final class ResourcesRoutes( } }, // Fetch a resource - (get & idSegmentRef(id)) { id => + (get & idSegmentRef(id) & varyAcceptHeaders) { id => emitOrFusionRedirect( ref, id, @@ -173,15 +173,16 @@ final class ResourcesRoutes( } }, // Fetch a resource original source - (pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(id)) { id => + (pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(id) & varyAcceptHeaders) { id => authorizeFor(ref, Read).apply { - parameter("annotate".as[Boolean].withDefault(false)) { annotate => + (parameter("annotate".as[Boolean].withDefault(false))) { annotate => implicit val source: Printer = sourcePrinter if (annotate) { emit( resources .fetch(id, ref, schemaOpt) .flatMap(asSourceWithMetadata) + .rejectWhen(wrongJsonOrNotFound) ) } else { val sourceIO = resources.fetch(id, ref, schemaOpt).map(_.value.source) diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala index da88aaa92c..b1ef9d1f86 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala @@ -1,7 +1,7 @@ package ch.epfl.bluebrain.nexus.delta.routes import akka.http.scaladsl.model.MediaTypes.`text/html` -import akka.http.scaladsl.model.headers.{Accept, Location, OAuth2BearerToken} +import akka.http.scaladsl.model.headers.{Accept, Location, OAuth2BearerToken, RawHeader} import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.server.Route import cats.effect.IO @@ -119,6 +119,8 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { private val payloadUpdatedWithMetdata = payloadWithMetadata deepMerge json"""{"name": "Alice", "address": null}""" + private val varyHeader = RawHeader("Vary", "Accept,Accept-Encoding") + "A resource route" should { "fail to create a resource without resources/write permission" in { @@ -359,6 +361,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { status shouldEqual StatusCodes.OK val meta = resourceMetadata(projectRef, myId, schemas.resources, "Custom", deprecated = true, rev = 10) response.asJson shouldEqual payloadUpdated.dropNullValues.deepMerge(meta).deepMerge(resourceCtx) + response.headers should contain(varyHeader) } } @@ -376,6 +379,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { Get(endpoint) ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual payload.deepMerge(meta).deepMerge(resourceCtx) + response.headers should contain(varyHeader) } } } @@ -440,6 +444,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { "id" -> "https://bluebrain.github.io/nexus/vocabulary/wrongid", "proj" -> "myorg/myproject" ) + response.headers should not contain varyHeader } } } @@ -448,6 +453,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { Get("/v1/resources/myorg/myproject/_/myid/source?annotate=true") ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual payloadUpdatedWithMetdata + response.headers should contain(varyHeader) } } @@ -468,6 +474,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { Get(endpoint) ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual payload.deepMerge(meta) + response.headers should contain(varyHeader) } } } @@ -485,6 +492,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { Get(endpoint) ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual payload + response.headers should contain(varyHeader) } } } diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala index 01db29f332..4755d974af 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala @@ -3,7 +3,7 @@ package ch.epfl.bluebrain.nexus.delta.sdk.directives import akka.http.scaladsl.model.MediaTypes.{`application/json`, `text/html`} import akka.http.scaladsl.model.StatusCodes.{Redirection, SeeOther} import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers.{`Last-Event-ID`, Accept} +import akka.http.scaladsl.model.headers.{`Accept-Encoding`, `Last-Event-ID`, Accept, RawHeader} import akka.http.scaladsl.server.ContentNegotiator.Alternative import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ @@ -110,6 +110,13 @@ trait DeltaDirectives extends UriDirectives { } } + /** Injects a `Vary: Accept,Accept-Encoding` into the response */ + def varyAcceptHeaders: Directive0 = + vary(Set(Accept.name, `Accept-Encoding`.name)) + + private def vary(headers: Set[String]): Directive0 = + respondWithHeader(RawHeader("Vary", headers.mkString(","))) + /** * If the `Accept` header is set to `text/html`, redirect to the matching resource page in fusion if the feature is * enabled From 3d27f2dc36b024fd11772583b88250431a33ef2e Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:15:46 +0200 Subject: [PATCH 2/5] Add vary header for files fetch operations --- .../plugins/storage/files/routes/FilesRoutes.scala | 10 +++++++--- .../storage/files/routes/FilesRoutesSpec.scala | 11 ++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala index e154de1d20..17c8f1b81f 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala @@ -184,13 +184,17 @@ final class FilesRoutes( } def fetch(id: IdSegmentRef, ref: ProjectRef)(implicit caller: Caller): Route = - headerValueByType(Accept) { + (headerValueByType(Accept) & varyAcceptHeaders) { case accept if accept.mediaRanges.exists(metadataMediaRanges.contains) => - emit(fetchMetadata(id, ref).rejectOn[FileNotFound]) + emit(fetchMetadata(id, ref).rejectWhen(unauthorizedOrNotFound)) case _ => - emit(files.fetchContent(id, ref).rejectOn[FileNotFound]) + emit(files.fetchContent(id, ref).rejectWhen(unauthorizedOrNotFound)) } + private val unauthorizedOrNotFound: PartialFunction[FileRejection, Boolean] = { + case _: AuthorizationFailed | _: FileNotFound => true + } + def fetchMetadata(id: IdSegmentRef, ref: ProjectRef)(implicit caller: Caller): IO[FileRejection, FileResource] = aclCheck.authorizeForOr(ref, Read)(AuthorizationFailed(ref, Read)) >> files.fetch(id, ref) } diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index ea5e957d8f..2d8cb6a800 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -4,7 +4,7 @@ import akka.actor.typed import akka.http.scaladsl.model.ContentTypes.`text/plain(UTF-8)` import akka.http.scaladsl.model.MediaRanges._ import akka.http.scaladsl.model.MediaTypes.`text/html` -import akka.http.scaladsl.model.headers.{Accept, Location, OAuth2BearerToken} +import akka.http.scaladsl.model.headers.{Accept, Location, OAuth2BearerToken, RawHeader} import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.server.Route import ch.epfl.bluebrain.nexus.delta.kernel.http.MediaTypeDetectorConfig @@ -126,6 +126,8 @@ class FilesRoutesSpec private val diskIdRev = ResourceRef.Revision(dId, 1) private val s3IdRev = ResourceRef.Revision(s3Id, 2) + private val varyHeader = RawHeader("Vary", "Accept,Accept-Encoding") + "File routes" should { "create storages for files" in { @@ -310,6 +312,7 @@ class FilesRoutesSpec Get(s"/v1/files/org/proj/file1$suffix") ~> Accept(`*/*`) ~> routes ~> check { response.status shouldEqual StatusCodes.Forbidden response.asJson shouldEqual jsonContentOf("errors/authorization-failed.json") + response.headers should not contain varyHeader } } } @@ -320,6 +323,7 @@ class FilesRoutesSpec Get(s"/v1/files/org/proj/file1$suffix") ~> Accept(`video/*`) ~> routes ~> check { response.status shouldEqual StatusCodes.NotAcceptable response.asJson shouldEqual jsonContentOf("errors/content-type.json", "expected" -> "text/plain") + response.headers should not contain varyHeader } } } @@ -336,6 +340,7 @@ class FilesRoutesSpec header("Content-Disposition").value.value() shouldEqual s"""attachment; filename="=?UTF-8?B?$filename64?="""" response.asString shouldEqual content + response.headers should contain(varyHeader) } } } @@ -362,6 +367,7 @@ class FilesRoutesSpec header("Content-Disposition").value.value() shouldEqual s"""attachment; filename="=?UTF-8?B?$filename64?="""" response.asString shouldEqual content + response.headers should contain(varyHeader) } } } @@ -375,6 +381,7 @@ class FilesRoutesSpec Get(s"$endpoint$suffix") ~> Accept(`application/ld+json`) ~> routes ~> check { response.status shouldEqual StatusCodes.Forbidden response.asJson shouldEqual jsonContentOf("errors/authorization-failed.json") + response.headers should not contain varyHeader } } } @@ -386,6 +393,7 @@ class FilesRoutesSpec status shouldEqual StatusCodes.OK val attr = attributes("file-idx-1.txt") response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 4, createdBy = alice) + response.headers should contain(varyHeader) } } @@ -406,6 +414,7 @@ class FilesRoutesSpec status shouldEqual StatusCodes.OK response.asJson shouldEqual fileMetadata(projectRef, file1, attr, s3IdRev, createdBy = alice, updatedBy = alice) + response.headers should contain(varyHeader) } } } From ca965501999e2a3f5ba740efceb4a7a27317bf2c Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:54:40 +0200 Subject: [PATCH 3/5] Update resources integration tests --- .../nexus/tests/kg/ResourcesSpec.scala | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala index bb8af47eb3..4c014a503a 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala @@ -1,7 +1,7 @@ package ch.epfl.bluebrain.nexus.tests.kg import akka.http.scaladsl.model.MediaTypes.`text/html` -import akka.http.scaladsl.model.headers.{Accept, Location} +import akka.http.scaladsl.model.headers.{Accept, Location, RawHeader} import akka.http.scaladsl.model.{MediaRange, StatusCodes} import akka.http.scaladsl.unmarshalling.PredefinedFromEntityUnmarshallers import cats.implicits._ @@ -35,6 +35,8 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { private val IdLens: Optional[Json, String] = root.`@id`.string private val TypeLens: Optional[Json, String] = root.`@type`.string + private val varyHeader = RawHeader("Vary", "Accept,Accept-Encoding") + private val resource1Id = "https://dev.nexus.test.com/simplified-resource/1" private def resource1Response(rev: Int, priority: Int) = SimpleResource.fetchResponse(Rick, id1, resource1Id, rev, priority) @@ -130,18 +132,24 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { } "fail to fetch the resource when the user does not have access" in { - deltaClient.get[Json](s"/resources/$id1/test-schema/test-resource:1", Anonymous) { expectForbidden } + deltaClient.get[Json](s"/resources/$id1/test-schema/test-resource:1", Anonymous) { (_, response) => + expectForbidden + response.headers should not contain varyHeader + } } "fail to fetch the original payload when the user does not have access" in { - deltaClient.get[Json](s"/resources/$id1/test-schema/test-resource:1/source", Anonymous) { + deltaClient.get[Json](s"/resources/$id1/test-schema/test-resource:1/source", Anonymous) { (_, response) => expectForbidden + response.headers should not contain varyHeader } } "fail to fetch the annotated original payload when the user does not have access" in { deltaClient.get[Json](s"/resources/$id1/test-schema/test-resource:1/source?annotate=true", Anonymous) { - expectForbidden + (_, response) => + expectForbidden + response.headers should not contain varyHeader } } @@ -150,6 +158,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { val expected = resource1Response(1, 5) response.status shouldEqual StatusCodes.OK filterMetadataKeys(json) should equalIgnoreArrayOrder(expected) + response.headers should contain(varyHeader) } } @@ -158,6 +167,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { val expected = SimpleResource.sourcePayload(resource1Id, 5) response.status shouldEqual StatusCodes.OK json should equalIgnoreArrayOrder(expected) + response.headers should contain(varyHeader) } } @@ -167,6 +177,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { response.status shouldEqual StatusCodes.OK val expected = resource1AnnotatedSource(1, 5) filterMetadataKeys(json) should equalIgnoreArrayOrder(expected) + response.headers should contain(varyHeader) } } @@ -179,6 +190,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { } _ <- deltaClient.get[Json](s"/resources/$id1/_/42/source?annotate=true", Morty) { (json, response) => response.status shouldEqual StatusCodes.OK + response.headers should contain(varyHeader) json should have(`@id`(s"42")) } } yield succeed @@ -198,6 +210,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { _ <- deltaClient.get[Json](s"/resources/$id1/_/${UrlUtils.encode(generatedId)}/source?annotate=true", Morty) { (json, response) => response.status shouldEqual StatusCodes.OK + response.headers should contain(varyHeader) json should have(`@id`(generatedId)) } } yield succeed @@ -207,6 +220,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { deltaClient.get[Json](s"/resources/$id1/test-schema/does-not-exist-resource:1/source?annotate=true", Morty) { (_, response) => response.status shouldEqual StatusCodes.NotFound + response.headers should not contain varyHeader } } @@ -215,6 +229,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { deltaClient.put[Json](s"/resources/$id2/test-schema/test-resource:1", payload, Rick) { (_, response) => response.status shouldEqual StatusCodes.NotFound + response.headers should not contain varyHeader } } @@ -225,6 +240,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { deltaClient.put[Json](s"/resources/$id2/_/test-resource:1", payload, Rick) { (_, response) => response.status shouldEqual StatusCodes.BadRequest + response.headers should not contain varyHeader } } } From 7f56d219e4c87fbbfdc66d2bf4b7430193cce978 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 6 Oct 2023 13:46:30 +0200 Subject: [PATCH 4/5] Lower impact on routes --- .../nexus/delta/routes/ResourcesRoutes.scala | 3 +-- .../storage/files/routes/FilesRoutes.scala | 8 ++----- .../sdk/directives/DeltaDirectives.scala | 22 +++++++++++++------ 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala index f1870270a1..f4c17f9785 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala @@ -175,14 +175,13 @@ final class ResourcesRoutes( // Fetch a resource original source (pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(id) & varyAcceptHeaders) { id => authorizeFor(ref, Read).apply { - (parameter("annotate".as[Boolean].withDefault(false))) { annotate => + parameter("annotate".as[Boolean].withDefault(false)) { annotate => implicit val source: Printer = sourcePrinter if (annotate) { emit( resources .fetch(id, ref, schemaOpt) .flatMap(asSourceWithMetadata) - .rejectWhen(wrongJsonOrNotFound) ) } else { val sourceIO = resources.fetch(id, ref, schemaOpt).map(_.value.source) diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala index 17c8f1b81f..d41f74e3cb 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala @@ -186,15 +186,11 @@ final class FilesRoutes( def fetch(id: IdSegmentRef, ref: ProjectRef)(implicit caller: Caller): Route = (headerValueByType(Accept) & varyAcceptHeaders) { case accept if accept.mediaRanges.exists(metadataMediaRanges.contains) => - emit(fetchMetadata(id, ref).rejectWhen(unauthorizedOrNotFound)) + emit(fetchMetadata(id, ref).rejectOn[FileNotFound]) case _ => - emit(files.fetchContent(id, ref).rejectWhen(unauthorizedOrNotFound)) + emit(files.fetchContent(id, ref).rejectOn[FileNotFound]) } - private val unauthorizedOrNotFound: PartialFunction[FileRejection, Boolean] = { - case _: AuthorizationFailed | _: FileNotFound => true - } - def fetchMetadata(id: IdSegmentRef, ref: ProjectRef)(implicit caller: Caller): IO[FileRejection, FileResource] = aclCheck.authorizeForOr(ref, Read)(AuthorizationFailed(ref, Read)) >> files.fetch(id, ref) } diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala index 4755d974af..231fc1a0b6 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala @@ -110,13 +110,6 @@ trait DeltaDirectives extends UriDirectives { } } - /** Injects a `Vary: Accept,Accept-Encoding` into the response */ - def varyAcceptHeaders: Directive0 = - vary(Set(Accept.name, `Accept-Encoding`.name)) - - private def vary(headers: Set[String]): Directive0 = - respondWithHeader(RawHeader("Vary", headers.mkString(","))) - /** * If the `Accept` header is set to `text/html`, redirect to the matching resource page in fusion if the feature is * enabled @@ -186,4 +179,19 @@ trait DeltaDirectives extends UriDirectives { /** The URI of fusion's main login page */ def fusionLoginUri(implicit config: FusionConfig): UIO[Uri] = UIO.pure { config.base / "login" } + + /** Injects a `Vary: Accept,Accept-Encoding` into the response */ + def varyAcceptHeaders: Directive0 = + vary(Set(Accept.name, `Accept-Encoding`.name)) + + private def vary(headers: Set[String]): Directive0 = + respondWithHeader(RawHeader("Vary", headers.mkString(","))) + + private def respondWithHeader(responseHeader: HttpHeader): Directive0 = + mapSuccessResponse(r => r.withHeaders(r.headers :+ responseHeader)) + + private def mapSuccessResponse(f: HttpResponse => HttpResponse): Directive0 = + mapRouteResultPF { + case RouteResult.Complete(response) if response.status.isSuccess => RouteResult.Complete(f(response)) + } } From 6d323cc5093dc906569a7ca47be9e6fed9278e4d Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 6 Oct 2023 13:53:55 +0200 Subject: [PATCH 5/5] Copy over the new directive to the cats file --- .../nexus/delta/sdk/ce/DeltaDirectives.scala | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/ce/DeltaDirectives.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/ce/DeltaDirectives.scala index ad57159de3..743850583b 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/ce/DeltaDirectives.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/ce/DeltaDirectives.scala @@ -3,7 +3,7 @@ package ch.epfl.bluebrain.nexus.delta.sdk.ce import akka.http.scaladsl.model.MediaTypes.{`application/json`, `text/html`} import akka.http.scaladsl.model.StatusCodes.{Redirection, SeeOther} import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers.{`Last-Event-ID`, Accept} +import akka.http.scaladsl.model.headers.{`Accept-Encoding`, `Last-Event-ID`, Accept, RawHeader} import akka.http.scaladsl.server.ContentNegotiator.Alternative import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ @@ -164,4 +164,19 @@ trait DeltaDirectives extends UriDirectives { } case None => provide(Offset.Start) } + + /** Injects a `Vary: Accept,Accept-Encoding` into the response */ + def varyAcceptHeaders: Directive0 = + vary(Set(Accept.name, `Accept-Encoding`.name)) + + private def vary(headers: Set[String]): Directive0 = + respondWithHeader(RawHeader("Vary", headers.mkString(","))) + + private def respondWithHeader(responseHeader: HttpHeader): Directive0 = + mapSuccessResponse(r => r.withHeaders(r.headers :+ responseHeader)) + + private def mapSuccessResponse(f: HttpResponse => HttpResponse): Directive0 = + mapRouteResultPF { + case RouteResult.Complete(response) if response.status.isSuccess => RouteResult.Complete(f(response)) + } }