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

okhttp3.logging.HttpLoggingInterceptor.kt Maven / Gradle / Ivy

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * 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 okhttp3.logging

import java.io.IOException
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets.UTF_8
import java.util.TreeSet
import java.util.concurrent.TimeUnit
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.internal.http.promisesBody
import okhttp3.internal.platform.Platform
import okio.Buffer
import okio.GzipSource

/**
 * An OkHttp interceptor which logs request and response information. Can be applied as an
 * [application interceptor][OkHttpClient.interceptors] or as a [OkHttpClient.networkInterceptors].
 *
 * The format of the logs created by this class should not be considered stable and may
 * change slightly between releases. If you need a stable logging format, use your own interceptor.
 */
class HttpLoggingInterceptor @JvmOverloads constructor(
  private val logger: Logger = Logger.DEFAULT
) : Interceptor {

  @Volatile private var headersToRedact = emptySet()

  @set:JvmName("level")
  @Volatile var level = Level.NONE

  enum class Level {
    /** No logs. */
    NONE,

    /**
     * Logs request and response lines.
     *
     * Example:
     * ```
     * --> POST /greeting http/1.1 (3-byte body)
     *
     * <-- 200 OK (22ms, 6-byte body)
     * ```
     */
    BASIC,

    /**
     * Logs request and response lines and their respective headers.
     *
     * Example:
     * ```
     * --> POST /greeting http/1.1
     * Host: example.com
     * Content-Type: plain/text
     * Content-Length: 3
     * --> END POST
     *
     * <-- 200 OK (22ms)
     * Content-Type: plain/text
     * Content-Length: 6
     * <-- END HTTP
     * ```
     */
    HEADERS,

    /**
     * Logs request and response lines and their respective headers and bodies (if present).
     *
     * Example:
     * ```
     * --> POST /greeting http/1.1
     * Host: example.com
     * Content-Type: plain/text
     * Content-Length: 3
     *
     * Hi?
     * --> END POST
     *
     * <-- 200 OK (22ms)
     * Content-Type: plain/text
     * Content-Length: 6
     *
     * Hello!
     * <-- END HTTP
     * ```
     */
    BODY
  }

  fun interface Logger {
    fun log(message: String)

    companion object {
      /** A [Logger] defaults output appropriate for the current platform. */
      @JvmField
      val DEFAULT: Logger = DefaultLogger()
      private class DefaultLogger : Logger {
        override fun log(message: String) {
          Platform.get().log(message)
        }
      }
    }
  }

  fun redactHeader(name: String) {
    val newHeadersToRedact = TreeSet(String.CASE_INSENSITIVE_ORDER)
    newHeadersToRedact += headersToRedact
    newHeadersToRedact += name
    headersToRedact = newHeadersToRedact
  }

  /**
   * Sets the level and returns this.
   *
   * This was deprecated in OkHttp 4.0 in favor of the [level] val. In OkHttp 4.3 it is
   * un-deprecated because Java callers can't chain when assigning Kotlin vals. (The getter remains
   * deprecated).
   */
  fun setLevel(level: Level) = apply {
    this.level = level
  }

  @JvmName("-deprecated_level")
  @Deprecated(
      message = "moved to var",
      replaceWith = ReplaceWith(expression = "level"),
      level = DeprecationLevel.ERROR
  )
  fun getLevel(): Level = level

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val level = this.level

    val request = chain.request()
    if (level == Level.NONE) {
      return chain.proceed(request)
    }

    val logBody = level == Level.BODY
    val logHeaders = logBody || level == Level.HEADERS

    val requestBody = request.body

    val connection = chain.connection()
    var requestStartMessage =
        ("--> ${request.method} ${request.url}${if (connection != null) " " + connection.protocol() else ""}")
    if (!logHeaders && requestBody != null) {
      requestStartMessage += " (${requestBody.contentLength()}-byte body)"
    }
    logger.log(requestStartMessage)

    if (logHeaders) {
      val headers = request.headers

      if (requestBody != null) {
        // Request body headers are only present when installed as a network interceptor. When not
        // already present, force them to be included (if available) so their values are known.
        requestBody.contentType()?.let {
          if (headers["Content-Type"] == null) {
            logger.log("Content-Type: $it")
          }
        }
        if (requestBody.contentLength() != -1L) {
          if (headers["Content-Length"] == null) {
            logger.log("Content-Length: ${requestBody.contentLength()}")
          }
        }
      }

      for (i in 0 until headers.size) {
        logHeader(headers, i)
      }

      if (!logBody || requestBody == null) {
        logger.log("--> END ${request.method}")
      } else if (bodyHasUnknownEncoding(request.headers)) {
        logger.log("--> END ${request.method} (encoded body omitted)")
      } else if (requestBody.isDuplex()) {
        logger.log("--> END ${request.method} (duplex request body omitted)")
      } else if (requestBody.isOneShot()) {
        logger.log("--> END ${request.method} (one-shot body omitted)")
      } else {
        val buffer = Buffer()
        requestBody.writeTo(buffer)

        val contentType = requestBody.contentType()
        val charset: Charset = contentType?.charset(UTF_8) ?: UTF_8

        logger.log("")
        if (buffer.isProbablyUtf8()) {
          logger.log(buffer.readString(charset))
          logger.log("--> END ${request.method} (${requestBody.contentLength()}-byte body)")
        } else {
          logger.log(
              "--> END ${request.method} (binary ${requestBody.contentLength()}-byte body omitted)")
        }
      }
    }

    val startNs = System.nanoTime()
    val response: Response
    try {
      response = chain.proceed(request)
    } catch (e: Exception) {
      logger.log("<-- HTTP FAILED: $e")
      throw e
    }

    val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)

    val responseBody = response.body!!
    val contentLength = responseBody.contentLength()
    val bodySize = if (contentLength != -1L) "$contentLength-byte" else "unknown-length"
    logger.log(
        "<-- ${response.code}${if (response.message.isEmpty()) "" else ' ' + response.message} ${response.request.url} (${tookMs}ms${if (!logHeaders) ", $bodySize body" else ""})")

    if (logHeaders) {
      val headers = response.headers
      for (i in 0 until headers.size) {
        logHeader(headers, i)
      }

      if (!logBody || !response.promisesBody()) {
        logger.log("<-- END HTTP")
      } else if (bodyHasUnknownEncoding(response.headers)) {
        logger.log("<-- END HTTP (encoded body omitted)")
      } else {
        val source = responseBody.source()
        source.request(Long.MAX_VALUE) // Buffer the entire body.
        var buffer = source.buffer

        var gzippedLength: Long? = null
        if ("gzip".equals(headers["Content-Encoding"], ignoreCase = true)) {
          gzippedLength = buffer.size
          GzipSource(buffer.clone()).use { gzippedResponseBody ->
            buffer = Buffer()
            buffer.writeAll(gzippedResponseBody)
          }
        }

        val contentType = responseBody.contentType()
        val charset: Charset = contentType?.charset(UTF_8) ?: UTF_8

        if (!buffer.isProbablyUtf8()) {
          logger.log("")
          logger.log("<-- END HTTP (binary ${buffer.size}-byte body omitted)")
          return response
        }

        if (contentLength != 0L) {
          logger.log("")
          logger.log(buffer.clone().readString(charset))
        }

        if (gzippedLength != null) {
          logger.log("<-- END HTTP (${buffer.size}-byte, $gzippedLength-gzipped-byte body)")
        } else {
          logger.log("<-- END HTTP (${buffer.size}-byte body)")
        }
      }
    }

    return response
  }

  private fun logHeader(headers: Headers, i: Int) {
    val value = if (headers.name(i) in headersToRedact) "██" else headers.value(i)
    logger.log(headers.name(i) + ": " + value)
  }

  private fun bodyHasUnknownEncoding(headers: Headers): Boolean {
    val contentEncoding = headers["Content-Encoding"] ?: return false
    return !contentEncoding.equals("identity", ignoreCase = true) &&
        !contentEncoding.equals("gzip", ignoreCase = true)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy