From 583e63b16e5ac3b8dc6447216ed662ddcf475612 Mon Sep 17 00:00:00 2001 From: Piotr Limanowski Date: Tue, 5 Dec 2023 13:29:53 +0100 Subject: [PATCH] Add gunzip middleware to support gzipped payloads An experiment in adding a simple middleware that will handle gzipped requests in the collector and process them as usual next. --- .../GUnzipMiddleware.scala | 41 +++++++++++++++++++ .../Routes.scala | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/GUnzipMiddleware.scala diff --git a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/GUnzipMiddleware.scala b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/GUnzipMiddleware.scala new file mode 100644 index 000000000..a825706ac --- /dev/null +++ b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/GUnzipMiddleware.scala @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2013-present Snowplow Analytics Ltd. + * All rights reserved. + * + * This program is licensed to you under the Snowplow Community License Version 1.0, + * and you may not use this file except in compliance with the Snowplow Community License Version 1.0. + * You may obtain a copy of the Snowplow Community License Version 1.0 at https://docs.snowplow.io/community-license-1.0 + */ +package com.snowplowanalytics.snowplow.collector.core + +import fs2.compression.Compression +import cats.data.Kleisli +import cats.effect._ +import org.http4s._ +import org.http4s.headers.`Content-Encoding` + +object GUnzipMiddleware { + def apply[F[_]: Sync](service: HttpRoutes[F], bufferSize: Int = 100 * 1024): HttpRoutes[F] = + Kleisli { (req: Request[F]) => + val req2 = req.headers.get[`Content-Encoding`] match { + case Some(header) if satisfiedByGzip(header) => + val decoded = req.body.through(Compression.forSync[F].gunzip(bufferSize)).flatMap(_.content).handleErrorWith { + e => + throw MalformedMessageBodyFailure( + "Failed to decode gzippped request body", + Some(e) + ) + } + req + .removeHeader[`Content-Encoding`] + .withEntity(decoded)( + EntityEncoder.entityBodyEncoder + ) // resolving implicit conflict + case _ => req + } + service(req2) + } + + private def satisfiedByGzip(header: `Content-Encoding`) = + header.contentCoding.matches(ContentCoding.gzip) || header.contentCoding.matches(ContentCoding.`x-gzip`) +} diff --git a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Routes.scala b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Routes.scala index 0e1e7f2ef..70e25ff5d 100644 --- a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Routes.scala +++ b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Routes.scala @@ -94,7 +94,7 @@ class Routes[F[_]: Sync]( val value: HttpApp[F] = { val routes = healthRoutes <+> corsRoute <+> cookieRoutes <+> rootRoute <+> crossdomainRoute - val res = if (enableDefaultRedirect) routes else rejectRedirect <+> routes + val res = if (enableDefaultRedirect) GUnzipMiddleware(routes) else rejectRedirect <+> routes res.orNotFound } }