All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.http4s.client.middleware.GZip.scala Maven / Gradle / Ivy

/*
 * Copyright 2014 http4s.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.http4s
package client
package middleware

import cats.ApplicativeThrow
import cats.data.NonEmptyList
import cats.effect.kernel.Async
import cats.effect.kernel.MonadCancelThrow
import fs2.Pipe
import fs2.Pull
import fs2.Stream
import fs2.compression.Compression
import fs2.compression.DeflateParams
import fs2.io.compression._
import org.http4s.headers.`Accept-Encoding`
import org.http4s.headers.`Content-Encoding`
import org.typelevel.ci._

import scala.util.control.NoStackTrace

/** Client middleware for enabling gzip.
  */
object GZip {
  private val supportedCompressions =
    NonEmptyList.of(ContentCoding.gzip, ContentCoding.deflate)

  def apply[F[_]: MonadCancelThrow: Compression](
      bufferSize: Int = 32 * 1024
  )(client: Client[F]): Client[F] =
    Client[F] { req =>
      val reqWithEncoding = addHeaders(req)
      val responseResource = client.run(reqWithEncoding)

      responseResource.map { actualResponse =>
        decompress(bufferSize, actualResponse)
      }
    }

  @deprecated("Use overload with `Compression` constraint", "0.23.17")
  def apply[F[_]](bufferSize: Int, client: Client[F], F: Async[F]): Client[F] = {
    implicit val async = F
    apply(bufferSize)(client)
  }

  private def addHeaders[F[_]](req: Request[F]): Request[F] =
    req.headers.get[`Accept-Encoding`] match {
      case Some(_) =>
        req
      case _ =>
        req.putHeaders(`Accept-Encoding`(supportedCompressions))
    }

  private def decompress[F[_]: ApplicativeThrow: Compression](
      bufferSize: Int,
      response: Response[F],
  ): Response[F] =
    response.headers.get[`Content-Encoding`] match {
      case Some(header)
          if header.contentCoding == ContentCoding.gzip || header.contentCoding == ContentCoding.`x-gzip` =>
        val gunzip: Pipe[F, Byte, Byte] =
          _.through(Compression[F].gunzip(bufferSize)).flatMap(_.content)
        response
          .filterHeaders(nonCompressionHeader)
          .pipeBodyThrough(decompressWith(gunzip))

      case Some(header) if header.contentCoding == ContentCoding.deflate =>
        val deflate: Pipe[F, Byte, Byte] = Compression[F].deflate(DeflateParams(bufferSize))
        response
          .filterHeaders(nonCompressionHeader)
          .pipeBodyThrough(decompressWith(deflate))

      case _ =>
        response
    }

  private def decompressWith[F[_]: ApplicativeThrow](
      decompressor: Pipe[F, Byte, Byte]
  ): Pipe[F, Byte, Byte] =
    _.pull.peek1
      .flatMap {
        case None => Pull.raiseError(EmptyBodyException)
        case Some((_, fullStream)) => Pull.output1(fullStream)
      }
      .stream
      .flatten
      .through(decompressor)
      .handleErrorWith {
        case EmptyBodyException => Stream.empty
        case error => Stream.raiseError(error)
      }

  private def nonCompressionHeader(header: Header.Raw): Boolean =
    header.name != ci"Content-Encoding" &&
      header.name != ci"Content-Length"

  private object EmptyBodyException extends Throwable with NoStackTrace
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy