Skip to content

客戶端自訂外掛

程式碼範例: client-custom-plugin

從 v2.2.0 開始,Ktor 提供了一組新的 API,用於建立自訂客戶端外掛。一般而言,此 API 不需要瞭解 Ktor 內部概念,例如 pipelines、phases 等等。相反地,您可以使用一組處理常式(例如 onRequestonResponse 等等),存取處理請求和回應的不同階段。

建立並安裝您的第一個外掛

在本節中,我們將展示如何建立並安裝您的第一個外掛,該外掛會為每個請求新增自訂標頭:

  1. 若要建立外掛,請呼叫 createClientPlugin 函式並傳入外掛名稱作為引數:

    kotlin
    package com.example.plugins
    
    import io.ktor.client.plugins.api.*
    
    val CustomHeaderPlugin = createClientPlugin("CustomHeaderPlugin") {
        // 設定外掛...
    }

    此函式會傳回 ClientPlugin 實例,該實例將用於安裝外掛。

  2. 若要為每個請求附加自訂標頭,您可以使用 onRequest 處理常式,它提供對請求參數的存取:

    kotlin
    package com.example.plugins
    
    import io.ktor.client.plugins.api.*
    
    val CustomHeaderPlugin = createClientPlugin("CustomHeaderPlugin") {
        onRequest { request, _ ->
            request.headers.append("X-Custom-Header", "Default value")
        }
    }
  3. 若要安裝外掛,請將建立的 ClientPlugin 實例傳遞給客戶端設定區塊內的 install 函式:

    kotlin
    import com.example.plugins.*
    
    val client = HttpClient(CIO) {
        install(CustomHeaderPlugin)
    }

您可以在此處找到完整範例:CustomHeader.kt。在以下章節中,我們將探討如何提供外掛設定以及處理請求和回應。

提供外掛設定

上一節展示了如何建立一個外掛,該外掛會為每個回應附加預定義的自訂標頭。讓我們使這個外掛更有用,並提供一個設定,用於傳遞任何自訂標頭名稱和值:

  1. 首先,您需要定義一個設定類別:

    kotlin
    class CustomHeaderPluginConfig {
        var headerName: String = "X-Custom-Header"
        var headerValue: String = "Default value"
    }
  2. 若要在外掛中使用此設定,請將設定類別參考傳遞給 createApplicationPlugin

    kotlin
    import io.ktor.client.plugins.api.*
    
    val CustomHeaderConfigurablePlugin = createClientPlugin("CustomHeaderConfigurablePlugin", ::CustomHeaderPluginConfig) {
        val headerName = pluginConfig.headerName
        val headerValue = pluginConfig.headerValue
    
        onRequest { request, _ ->
            request.headers.append(headerName, headerValue)
        }
    }

    鑑於外掛設定欄位是可變的,建議將它們儲存到區域變數中。

  3. 最後,您可以如下所示安裝並設定此外掛:

    kotlin
    val client = HttpClient(CIO) {
        install(CustomHeaderConfigurablePlugin) {
            headerName = "X-Custom-Header"
            headerValue = "Hello, world!"
        }
    }

您可以在此處找到完整範例:CustomHeaderConfigurable.kt

處理請求和回應

自訂外掛使用一組專用處理常式,提供對處理請求和回應不同階段的存取,例如:

  • onRequestonResponse 分別允許您處理請求和回應。
  • transformRequestBodytransformResponseBody 可用於對請求和回應主體應用必要的轉換。

還有 on(...) 處理常式,它允許您呼叫特定的掛鉤 (hook),這對於處理呼叫的其他階段可能很有用。 下表列出了所有處理常式及其執行順序:

處理常式 描述
onRequest

此處理常式會對每個 HTTP

請求
了解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求的主體。
執行,並允許您修改它。

範例:自訂標頭

transformRequestBody

允許您轉換請求主體。 在此處理常式中,您需要將主體序列化為 OutgoingContent (例如,TextContentByteArrayContentFormDataContent) 或者,如果您的轉換不適用,則傳回 null

範例:資料轉換

onResponse

此處理常式會對每個傳入的 HTTP

回應
了解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求的主體。
執行,並允許您以各種方式檢查它:記錄回應、儲存 Cookie 等等。

範例:記錄標頭回應時間

transformResponseBody

允許您轉換回應主體。 每次呼叫 HttpResponse.body 時,此處理常式都會被呼叫。 您需要將主體解序列化為 requestedType 的實例 或者,如果您的轉換不適用,則傳回 null

範例:資料轉換

onClose 允許您清理此外掛所分配的資源。 當客戶端關閉時,會呼叫此處理常式。
處理常式 描述
on(SetupRequest) 在請求處理中,SetupRequest 掛鉤會最先執行。
onRequest

此處理常式會對每個 HTTP

請求
了解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求的主體。
執行,並允許您修改它。

範例:自訂標頭

transformRequestBody

允許您轉換請求主體。 在此處理常式中,您需要將主體序列化為 OutgoingContent (例如,TextContentByteArrayContentFormDataContent) 或者,如果您的轉換不適用,則傳回 null

範例:資料轉換

on(Send)

Send 掛鉤提供檢查回應並在需要時啟動額外請求的能力。 這對於處理重新導向、重試請求、驗證等等可能很有用。

範例:驗證

on(SendingRequest)

即使請求不是由使用者啟動,SendingRequest 掛鉤也會對每個請求執行。 例如,如果請求導致重新導向,則 onRequest 處理常式只會對原始請求執行,而 on(SendingRequest) 則會對原始請求和重新導向的請求都執行。 同樣地,如果您使用 on(Send) 啟動額外請求,處理常式將按以下順序排列:

Console

範例:記錄標頭回應時間

onResponse

此處理常式會對每個傳入的 HTTP

回應
了解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求的主體。
執行,並允許您以各種方式檢查它:記錄回應、儲存 Cookie 等等。

範例:記錄標頭回應時間

transformResponseBody

允許您轉換回應主體。 每次呼叫 HttpResponse.body 時,此處理常式都會被呼叫。 您需要將主體解序列化為 requestedType 的實例 或者,如果您的轉換不適用,則傳回 null

範例:資料轉換

onClose 允許您清理此外掛所分配的資源。 當客戶端關閉時,會呼叫此處理常式。

共用呼叫狀態

自訂外掛允許您共用與呼叫相關的任何值,以便您能夠在任何處理此呼叫的處理常式內部存取此值。此值會儲存為在 call.attributes 集合中帶有唯一的鍵的屬性。以下範例展示了如何使用屬性計算傳送請求和接收回應之間的時間:

kotlin
import io.ktor.client.plugins.api.*
import io.ktor.util.*

val ResponseTimePlugin = createClientPlugin("ResponseTimePlugin") {
    val onCallTimeKey = AttributeKey<Long>("onCallTimeKey")
    on(SendingRequest) { request, content ->
        val onCallTime = System.currentTimeMillis()
        request.attributes.put(onCallTimeKey, onCallTime)
    }

    onResponse { response ->
        val onCallTime = response.call.attributes[onCallTimeKey]
        val onCallReceiveTime = System.currentTimeMillis()
        println("Read response delay (ms): ${onCallReceiveTime - onCallTime}")
    }
}

在此處可以找到完整範例:ResponseTime.kt

存取客戶端設定

您可以使用 client 屬性存取您的客戶端設定,它會傳回 HttpClient 實例。以下範例展示了如何取得客戶端使用的代理位址

kotlin
import io.ktor.client.plugins.api.*

val SimplePlugin = createClientPlugin("SimplePlugin") {
    val proxyAddress = client.engineConfig.proxy?.address()
    println("Proxy address: $proxyAddress")
}

範例

以下程式碼範例展示了自訂外掛的幾個範例。您可以在此處找到最終專案:client-custom-plugin

自訂標頭

展示如何建立為每個請求新增自訂標頭的外掛:

kotlin
package com.example.plugins

import io.ktor.client.plugins.api.*

val CustomHeaderConfigurablePlugin = createClientPlugin("CustomHeaderConfigurablePlugin", ::CustomHeaderPluginConfig) {
    val headerName = pluginConfig.headerName
    val headerValue = pluginConfig.headerValue

    onRequest { request, _ ->
        request.headers.append(headerName, headerValue)
    }
}

class CustomHeaderPluginConfig {
    var headerName: String = "X-Custom-Header"
    var headerValue: String = "Default value"
}

記錄標頭

展示如何建立記錄請求和回應標頭的外掛:

kotlin
package com.example.plugins

import io.ktor.client.plugins.api.*

val LoggingHeadersPlugin = createClientPlugin("LoggingHeadersPlugin") {
    on(SendingRequest) { request, content ->
        println("Request headers:")
        request.headers.entries().forEach { entry ->
            printHeader(entry)
        }
    }

    onResponse { response ->
        println("Response headers:")
        response.headers.entries().forEach { entry ->
            printHeader(entry)
        }
    }
}

private fun printHeader(entry: Map.Entry<String, List<String>>) {
    var headerString = entry.key + ": "
    entry.value.forEach { headerValue ->
        headerString += "${headerValue};"
    }
    println("-> $headerString")
}

回應時間

展示如何建立測量傳送請求和接收回應之間時間的外掛:

kotlin
package com.example.plugins

import io.ktor.client.plugins.api.*
import io.ktor.util.*

val ResponseTimePlugin = createClientPlugin("ResponseTimePlugin") {
    val onCallTimeKey = AttributeKey<Long>("onCallTimeKey")
    on(SendingRequest) { request, content ->
        val onCallTime = System.currentTimeMillis()
        request.attributes.put(onCallTimeKey, onCallTime)
    }

    onResponse { response ->
        val onCallTime = response.call.attributes[onCallTimeKey]
        val onCallReceiveTime = System.currentTimeMillis()
        println("Read response delay (ms): ${onCallReceiveTime - onCallTime}")
    }
}

資料轉換

展示如何使用 transformRequestBodytransformResponseBody 掛鉤轉換請求和回應主體:

kotlin
package com.example.plugins

import com.example.model.*
import io.ktor.client.plugins.api.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.utils.io.*

val DataTransformationPlugin = createClientPlugin("DataTransformationPlugin") {
    transformRequestBody { request, content, bodyType ->
        if (bodyType?.type == User::class) {
            val user = content as User
            TextContent(text="${user.name};${user.age}", contentType = ContentType.Text.Plain)
        } else {
            null
        }
    }
    transformResponseBody { response, content, requestedType ->
        if (requestedType.type == User::class) {
            val receivedContent = content.readUTF8Line()!!.split(";")
            User(receivedContent[0], receivedContent[1].toInt())
        } else {
            content
        }
    }
}
kotlin
package com.example

import com.example.model.*
import com.example.plugins.*
import com.example.server.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*

fun main() {
    startServer()
    runBlocking {
        val client = HttpClient(CIO) {
            install(DataTransformationPlugin)
        }
        val bodyAsText = client.post("http://0.0.0.0:8080/post-data") {
            setBody(User("John", 42))
        }.bodyAsText()
        val user = client.get("http://0.0.0.0:8080/get-data").body<User>()
        println("Userinfo: $bodyAsText")
        println("Username: ${user.name}, age: ${user.age}")
    }
}
kotlin
package com.example.model

data class User(val name: String, val age: Int)

在此處可以找到完整範例:client-custom-plugin-data-transformation

驗證

一個 Ktor 範例專案,展示如何使用 on(Send) 掛鉤,在從伺服器收到未經授權的回應時,將承載權杖新增到 Authorization 標頭:

kotlin
package com.example.plugins

import io.ktor.client.plugins.api.*
import io.ktor.http.*

val AuthPlugin = createClientPlugin("AuthPlugin", ::AuthPluginConfig) {
    val token = pluginConfig.token

    on(Send) { request ->
        val originalCall = proceed(request)
        originalCall.response.run { // this: HttpResponse
            if(status == HttpStatusCode.Unauthorized && headers["WWW-Authenticate"]!!.contains("Bearer")) {
                request.headers.append("Authorization", "Bearer $token")
                proceed(request)
            } else {
                originalCall
            }
        }
    }
}

class AuthPluginConfig {
    var token: String = ""
}
kotlin
package com.example

import com.example.plugins.*
import com.example.server.*
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*

fun main() {
    startServer()
    runBlocking {
        val client = HttpClient(CIO) {
            install(AuthPlugin) {
                token = "abc123"
            }
        }
        val response = client.get("http://0.0.0.0:8080/")
        println(response.bodyAsText())
    }
}

在此處可以找到完整範例:client-custom-plugin-auth