iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0

昨天我們看過 val response: HttpResponse = client.get("https://ktor.io/") 的一些邏輯

今天我們來看看實際上取得的 HttpResponse 物件裡面的結構

以及我們可以怎麼使用它。

HttpClient.get 所拿到的 HttpResponse 實作如下

/**
 * An [HttpClient]'s response, a second part of [HttpClientCall].
 *
 * Learn more from [Receiving responses](https://ktor.io/docs/response.html).
 */
public abstract class HttpResponse : HttpMessage, CoroutineScope {
    /**
     * The associated [HttpClientCall] containing both
     * the underlying [HttpClientCall.request] and [HttpClientCall.response].
     */
    public abstract val call: HttpClientCall

    /**
     * The [HttpStatusCode] returned by the server. It includes both,
     * the [HttpStatusCode.description] and the [HttpStatusCode.value] (code).
     */
    public abstract val status: HttpStatusCode

    /**
     * HTTP version. Usually [HttpProtocolVersion.HTTP_1_1] or [HttpProtocolVersion.HTTP_2_0].
     */
    public abstract val version: HttpProtocolVersion

    /**
     * [GMTDate] of the request start.
     */
    public abstract val requestTime: GMTDate

    /**
     * [GMTDate] of the response start.
     */
    public abstract val responseTime: GMTDate

    /**
     * Unmodified [ByteReadChannel] with the raw payload of the response.
     *
     * **Note:** this content doesn't go through any interceptors from [HttpResponsePipeline].
     * If you need the modified content, use the [bodyChannel] function.
     */
    @InternalAPI
    public abstract val content: ByteReadChannel

    override fun toString(): String = "HttpResponse[${request.url}, $status]"
}

這邊我們可以看到 status 這個 HttpStatusCode 物件,也就是我們在

println(response.status)

這段範例程式所看到的物件。

HttpStatusCode 我們在 Day 06 時我們看過,這次我們花點時間看看其實作

public data class HttpStatusCode(val value: Int, val description: String) : Comparable<HttpStatusCode> {
    override fun toString(): String = "$value $description"

    override fun equals(other: Any?): Boolean = other is HttpStatusCode && other.value == value

    override fun hashCode(): Int = value.hashCode()

    /**
     * Returns a copy of `this` code with a description changed to [value].
     */
    public fun description(value: String): HttpStatusCode = copy(description = value)

    override fun compareTo(other: HttpStatusCode): Int = value - other.value

以上這段包含了 HttpStatusCode 的行為

至於實際定義每個 Status Code 的部分,這邊使用了 Kotlin 的 companion object

    @Suppress("KDocMissingDocumentation", "PublicApiImplicitType")
    public companion object {
        // =============================================================================================================
        // Disclaimer
        // Adding a new status code here please remember [allStatusCodes] as well
        //

        public val Continue: HttpStatusCode = HttpStatusCode(100, "Continue")
        public val SwitchingProtocols: HttpStatusCode = HttpStatusCode(101, "Switching Protocols")
        public val Processing: HttpStatusCode = HttpStatusCode(102, "Processing")

        public val OK: HttpStatusCode = HttpStatusCode(200, "OK")
        public val Created: HttpStatusCode = HttpStatusCode(201, "Created")
        public val Accepted: HttpStatusCode = HttpStatusCode(202, "Accepted")

        public val NonAuthoritativeInformation: HttpStatusCode =
            HttpStatusCode(203, "Non-Authoritative Information")

        public val NoContent: HttpStatusCode = HttpStatusCode(204, "No Content")
        public val ResetContent: HttpStatusCode = HttpStatusCode(205, "Reset Content")
        public val PartialContent: HttpStatusCode = HttpStatusCode(206, "Partial Content")
        public val MultiStatus: HttpStatusCode = HttpStatusCode(207, "Multi-Status")

        public val MultipleChoices: HttpStatusCode = HttpStatusCode(300, "Multiple Choices")
        public val MovedPermanently: HttpStatusCode = HttpStatusCode(301, "Moved Permanently")
        public val Found: HttpStatusCode = HttpStatusCode(302, "Found")
        public val SeeOther: HttpStatusCode = HttpStatusCode(303, "See Other")
        public val NotModified: HttpStatusCode = HttpStatusCode(304, "Not Modified")
        public val UseProxy: HttpStatusCode = HttpStatusCode(305, "Use Proxy")
        public val SwitchProxy: HttpStatusCode = HttpStatusCode(306, "Switch Proxy")
        public val TemporaryRedirect: HttpStatusCode = HttpStatusCode(307, "Temporary Redirect")
        public val PermanentRedirect: HttpStatusCode = HttpStatusCode(308, "Permanent Redirect")

        public val BadRequest: HttpStatusCode = HttpStatusCode(400, "Bad Request")
        public val Unauthorized: HttpStatusCode = HttpStatusCode(401, "Unauthorized")
        public val PaymentRequired: HttpStatusCode = HttpStatusCode(402, "Payment Required")
        public val Forbidden: HttpStatusCode = HttpStatusCode(403, "Forbidden")
        public val NotFound: HttpStatusCode = HttpStatusCode(404, "Not Found")
        public val MethodNotAllowed: HttpStatusCode = HttpStatusCode(405, "Method Not Allowed")
        public val NotAcceptable: HttpStatusCode = HttpStatusCode(406, "Not Acceptable")

        public val ProxyAuthenticationRequired: HttpStatusCode =
            HttpStatusCode(407, "Proxy Authentication Required")

        public val RequestTimeout: HttpStatusCode = HttpStatusCode(408, "Request Timeout")
        public val Conflict: HttpStatusCode = HttpStatusCode(409, "Conflict")
        public val Gone: HttpStatusCode = HttpStatusCode(410, "Gone")
        public val LengthRequired: HttpStatusCode = HttpStatusCode(411, "Length Required")
        public val PreconditionFailed: HttpStatusCode = HttpStatusCode(412, "Precondition Failed")
        public val PayloadTooLarge: HttpStatusCode = HttpStatusCode(413, "Payload Too Large")
        public val RequestURITooLong: HttpStatusCode = HttpStatusCode(414, "Request-URI Too Long")

        public val UnsupportedMediaType: HttpStatusCode = HttpStatusCode(415, "Unsupported Media Type")

        public val RequestedRangeNotSatisfiable: HttpStatusCode =
            HttpStatusCode(416, "Requested Range Not Satisfiable")

        public val ExpectationFailed: HttpStatusCode = HttpStatusCode(417, "Expectation Failed")
        public val UnprocessableEntity: HttpStatusCode = HttpStatusCode(422, "Unprocessable Entity")
        public val Locked: HttpStatusCode = HttpStatusCode(423, "Locked")
        public val FailedDependency: HttpStatusCode = HttpStatusCode(424, "Failed Dependency")
        public val TooEarly: HttpStatusCode = HttpStatusCode(425, "Too Early")
        public val UpgradeRequired: HttpStatusCode = HttpStatusCode(426, "Upgrade Required")
        public val TooManyRequests: HttpStatusCode = HttpStatusCode(429, "Too Many Requests")

        public val RequestHeaderFieldTooLarge: HttpStatusCode =
            HttpStatusCode(431, "Request Header Fields Too Large")

        public val InternalServerError: HttpStatusCode = HttpStatusCode(500, "Internal Server Error")
        public val NotImplemented: HttpStatusCode = HttpStatusCode(501, "Not Implemented")
        public val BadGateway: HttpStatusCode = HttpStatusCode(502, "Bad Gateway")
        public val ServiceUnavailable: HttpStatusCode = HttpStatusCode(503, "Service Unavailable")
        public val GatewayTimeout: HttpStatusCode = HttpStatusCode(504, "Gateway Timeout")

        public val VersionNotSupported: HttpStatusCode =
            HttpStatusCode(505, "HTTP Version Not Supported")

        public val VariantAlsoNegotiates: HttpStatusCode = HttpStatusCode(506, "Variant Also Negotiates")
        public val InsufficientStorage: HttpStatusCode = HttpStatusCode(507, "Insufficient Storage")

        /**
         * All known status codes
         */
        public val allStatusCodes: List<HttpStatusCode> = allStatusCodes()

        private val statusCodesMap: Map<Int, HttpStatusCode> = allStatusCodes.associateBy { it.value }

        /**
         * Creates an instance of [HttpStatusCode] with the given numeric value.
         */
        public fun fromValue(value: Int): HttpStatusCode {
            return statusCodesMap[value] ?: HttpStatusCode(value, "Unknown Status Code")
        }
    }

這邊可以看到對應每個 Status Code 的數值以及概述。

除了拿到 HTTP Status Code 之外

一個很基本的功能,是拿到 API 回傳的 Body

val response: HttpResponse = client.get("https://ktor.io/")
val stringBody: String = response.body()

Ktor Client 這邊有趣的是還可以轉換成 ByteArray

val response: HttpResponse = client.get("https://ktor.io/")
val byteArrayBody: ByteArray = response.body()

body 的實作如下

public suspend inline fun <reified T> HttpResponse.body(): T = call.bodyNullable(typeInfo<T>()) as T

到這邊我們就可以發現,為什麼前面的回傳型態可以定義成 String 或者是 ByteArray

因為在這邊回傳時,就會透過 as T 試著轉換型態了

至於轉換的型態資訊,則會透過 typeInfo<T>() 傳送給 bodyNullable

接著我們看 bodyNullable

    /**
     * Tries to receive the payload of the [response] as a specific expected type provided in [info].
     * Returns [response] if [info] corresponds to [HttpResponse].
     *
     * @throws NoTransformationFoundException If no transformation is found for the type [info].
     * @throws DoubleReceiveException If already called [body].
     */
    @OptIn(InternalAPI::class)
    public suspend fun bodyNullable(info: TypeInfo): Any? {
        try {
            if (response.instanceOf(info.type)) return response
            if (!allowDoubleReceive && !received.compareAndSet(false, true)) {
                throw DoubleReceiveException(this)
            }

            @Suppress("DEPRECATION_ERROR")
            val responseData = attributes.getOrNull(CustomResponse) ?: getResponseContent()

            val subject = HttpResponseContainer(info, responseData)
            val result = client.responsePipeline.execute(this, subject).response.takeIf { it != NullBody }

            if (result != null && !result.instanceOf(info.type)) {
                val from = result::class
                val to = info.type
                throw NoTransformationFoundException(response, from, to)
            }

            return result
        } catch (cause: Throwable) {
            response.cancel("Receive failed", cause)
            throw cause
        } finally {
            response.complete()
        }
    }

這邊我們看到幾個之前看過的類別,像是 TypeInfo,還有處理收到多次回應的 DoubleReceiveException 等。

val responseData = attributes.getOrNull(CustomResponse) ?: getResponseContent()
protected open suspend fun getResponseContent(): ByteReadChannel = response.content

上面首先將拿到的 response.content 送入 responseData

接著

val subject = HttpResponseContainer(info, responseData)

建立一個 HttpResponseContainer 物件

/**
 * Class representing a typed [response] with an attached [expectedType].
 * @param expectedType: information about expected type.
 * @param response: current response state.
 */
public data class HttpResponseContainer(val expectedType: TypeInfo, val response: Any)

接著是

val result = client.responsePipeline.execute(this, subject).response.takeIf { it != NullBody }

這一串先是透過 client.responsePipeline.execute

處理我們輸入的 HttpResponseContainer

接著我們就取得了 API 回應的 body 了。


上一篇
Day 16:存取第三方 API,HttpClient 和 client.get
下一篇
Day 18:client.post 和 setBody
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言