iT邦幫忙

2023 iThome 鐵人賽

DAY 18
0

前幾天,我們看過了 client.get("https://ktor.io/")  怎麼存取第三方 API 並取得資料。

今天,我們要來看看另一種存取第三方 API 的方式: POST

官方教學的寫法如下

val response: HttpResponse = client.post("http://localhost:8080/post") {
    setBody("Body content")
}

這邊的 setBody 不只可以放一段文字,也可以放物件

val response: HttpResponse = client.post("http://localhost:8080/customer") {
    contentType(ContentType.Application.Json)
    setBody(Customer(3, "Jet", "Brains"))
}

這裡面的 post 實作如下

/**
 * Executes an [HttpClient]'s POST request with the specified [url] and
 * an optional [block] receiving an [HttpRequestBuilder] for configuring the request.
 *
 * Learn more from [Making requests](https://ktor.io/docs/request.html).
 */
public suspend inline fun HttpClient.post(
    urlString: String,
    block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse = post { url(urlString); block() }

block 裡面定義為 HttpRequestBuilder.() -> Unit,就可以善用 HttpRequestBuilder 來協助建立我們的 HttpRequest

這邊的 url(urlString) 則是

public fun HttpRequestBuilder.url(urlString: String): Unit { // ktlint-disable filename no-unit-return
    url.takeFrom(urlString)
}
/**
 * [URLBuilder] to configure the URL for this request.
 */
public val url: URLBuilder = URLBuilder()

這邊利用 URLBuilder 來協助建立存取的網址。

除了這種寫法以外,也可以使用這種寫法

client.get {
    url {
        protocol = URLProtocol.HTTPS
        host = "ktor.io"
        path("docs/welcome.html")
    }
}

這個 url 會呼叫

/**
 * Executes a [block] that configures the [URLBuilder] associated to this request.
 */
public fun url(block: URLBuilder.(URLBuilder) -> Unit): Unit = url.block(url)

得到一樣的 URLBuilder

看完 url(),我們繼續看 post

/**
 * Executes an [HttpClient]'s POST request with the parameters configured in [block].
 *
 * Learn more from [Making requests](https://ktor.io/docs/request.html).
 */
public suspend inline fun HttpClient.post(block: HttpRequestBuilder.() -> Unit): HttpResponse =
    post(HttpRequestBuilder().apply(block))
/**
 * Executes an [HttpClient]'s POST request with the parameters configured in [builder].
 *
 * Learn more from [Making requests](https://ktor.io/docs/request.html).
 */
public suspend inline fun HttpClient.post(builder: HttpRequestBuilder): HttpResponse {
    builder.method = HttpMethod.Post
    return request(builder)
}

我們可以發現,這段跟 HttpClient.get 的做法非常相似,只是在 HttpClient.request 之前將 builder.method 改成 HttpMethod.Post 而已

這樣的結構還允許了另一種寫法

val response: HttpResponse = client.request("https://ktor.io/") {
    method = HttpMethod.Post
}

直接使用 HttpClient.request ,然後抽換掉 HttpRequestBuilder.method 也可以達成一樣的效果。

接著我們看 HttpRequestBuilder.setBody()

@OptIn(InternalAPI::class)
public inline fun <reified T> HttpRequestBuilder.setBody(body: T) {
    when (body) {
        null -> {
            this.body = NullBody
            bodyType = typeInfo<T>()
        }
        is OutgoingContent -> {
            this.body = body
            bodyType = null
        }
        else -> {
            this.body = body
            bodyType = typeInfo<T>()
        }
    }
}

NullBody 是一個 singleton,用來簡單的代表為空的請求或者回傳

/**
 * A subject of pipeline when body of HTTP message is `null`
 */
public object NullBody

接著,如果型態是 OutgoingContent 則直接設置 body

否則就建立一個 TypeInfo 物件,來記錄 body 對應的型態。

除了可以 setBody 以外,還可以用 HttpRequestBuilder.contentType 來設置 Content-Type

/**
 * Set `Content-Type` header.
 */
public fun HttpMessageBuilder.contentType(type: ContentType): Unit =
    headers.set(HttpHeaders.ContentType, type.toString())

ContentType.Application 也是一個 Singleton,裡面包含了各種可以用的 Content-Type

/**
 * Provides a list of standard subtypes of an `application` content type.
 */
@Suppress("KDocMissingDocumentation", "unused")
public object Application {
	/**
	 * Represents a pattern `application / *` to match any application content type.
	 */
	public val Any: ContentType = ContentType("application", "*")
	public val Atom: ContentType = ContentType("application", "atom+xml")
	public val Cbor: ContentType = ContentType("application", "cbor")
	public val Json: ContentType = ContentType("application", "json")
	public val HalJson: ContentType = ContentType("application", "hal+json")
	public val JavaScript: ContentType = ContentType("application", "javascript")
	public val OctetStream: ContentType = ContentType("application", "octet-stream")
	public val Rss: ContentType = ContentType("application", "rss+xml")
	public val Xml: ContentType = ContentType("application", "xml")
	public val Xml_Dtd: ContentType = ContentType("application", "xml-dtd")
	public val Zip: ContentType = ContentType("application", "zip")
	public val GZip: ContentType = ContentType("application", "gzip")

	public val FormUrlEncoded: ContentType =
		ContentType("application", "x-www-form-urlencoded")

	public val Pdf: ContentType = ContentType("application", "pdf")
	public val Xlsx: ContentType = ContentType(
		"application",
		"vnd.openxmlformats-officedocument.spreadsheetml.sheet"
	)
	public val Docx: ContentType = ContentType(
		"application",
		"vnd.openxmlformats-officedocument.wordprocessingml.document"
	)
	public val Pptx: ContentType = ContentType(
		"application",
		"vnd.openxmlformats-officedocument.presentationml.presentation"
	)
	public val ProtoBuf: ContentType = ContentType("application", "protobuf")
	public val Wasm: ContentType = ContentType("application", "wasm")
	public val ProblemJson: ContentType = ContentType("application", "problem+json")
	public val ProblemXml: ContentType = ContentType("application", "problem+xml")
}

這個物件可以用來協助我們更簡單的定義 header 的內容。

到今天,我們看過了如何用 Ktor 發送 POST 請求,並且看了框架背後運行的邏輯。

希望對各位讀者有幫助!


上一篇
Day 17:HttpResponse 的結構,以及 HttpResponse.body()
下一篇
Day 19:用 submitForm 以 FORM DATA 的形式傳遞資料
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言