Multiplatform asynchronous HTTP client core library for JVM that provides request/response handling, plugin architecture, and extensible HTTP communication capabilities.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Content serialization, form data, file uploads, and multipart support with type-safe handling. The content handling system provides comprehensive support for various content types, including forms, files, JSON, and custom serialization formats.
Support for URL-encoded and multipart form data with builder DSL.
/**
* Form URL-encoded content (application/x-www-form-urlencoded)
*/
class FormDataContent(
val formData: Parameters
) : OutgoingContent.ByteArrayContent() {
override val contentLength: Long
override val contentType: ContentType
override fun bytes(): ByteArray
}
/**
* Multipart form data content (multipart/form-data)
*/
class MultiPartFormDataContent(
parts: List<PartData>,
val boundary: String = generateBoundary(),
override val contentType: ContentType = ContentType.MultiPart.FormData.withParameter("boundary", boundary)
) : OutgoingContent.WriteChannelContent() {
override var contentLength: Long?
override suspend fun writeTo(channel: ByteWriteChannel)
}
/**
* Generate a unique boundary for multipart content
*/
fun generateBoundary(): StringConvenience functions for submitting forms with various content types.
/**
* Submit form data using URL encoding or query parameters
*/
suspend fun HttpClient.submitForm(
url: String,
formParameters: Parameters,
encodeInQuery: Boolean = false,
block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse
suspend fun HttpClient.submitForm(
url: Url,
formParameters: Parameters,
encodeInQuery: Boolean = false,
block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse
/**
* Submit multipart form data with binary content
*/
suspend fun HttpClient.submitFormWithBinaryData(
url: String,
formData: List<PartData>,
block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse
suspend fun HttpClient.submitFormWithBinaryData(
url: Url,
formData: List<PartData>,
block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse
/**
* Prepare form submission statements
*/
fun HttpClient.prepareForm(
url: String,
formParameters: Parameters,
encodeInQuery: Boolean = false,
block: HttpRequestBuilder.() -> Unit = {}
): HttpStatement
fun HttpClient.prepareFormWithBinaryData(
url: String,
formData: List<PartData>,
block: HttpRequestBuilder.() -> Unit = {}
): HttpStatementDSL for building form data with type-safe parameter handling.
/**
* Build multipart form data using DSL
*/
fun formData(block: FormBuilder.() -> Unit): List<PartData>
/**
* Builder for constructing multipart form data
*/
class FormBuilder {
/**
* Append a text part
*/
fun append(key: String, value: String, headers: Headers = Headers.Empty)
/**
* Append a number part
*/
fun append(key: String, value: Number, headers: Headers = Headers.Empty)
/**
* Append a byte array part
*/
fun append(key: String, value: ByteArray, headers: Headers = Headers.Empty)
/**
* Append a channel content part
*/
fun append(key: String, value: ByteReadChannel, headers: Headers = Headers.Empty)
/**
* Append a file part (JVM-specific)
*/
fun append(key: String, file: File, headers: Headers = Headers.Empty)
/**
* Append content with custom headers and filename
*/
fun append(
key: String,
value: ByteArray,
headers: Headers = Headers.Empty,
filename: String? = null
)
/**
* Append streaming content
*/
fun append(
key: String,
provider: ChannelProvider,
headers: Headers = Headers.Empty,
filename: String? = null
)
}
/**
* Channel provider for streaming content
*/
fun interface ChannelProvider {
fun invoke(): ByteReadChannel
}JVM-specific file content handling for uploads and serving.
/**
* JVM file content for uploading files
*/
class LocalFileContent(
val file: File,
override val contentType: ContentType = ContentType.defaultForFile(file)
) : OutgoingContent.ReadChannelContent() {
/** File size in bytes */
override val contentLength: Long = file.length()
/**
* Read the entire file content
*/
override fun readFrom(): ByteReadChannel
/**
* Read a range of the file content
*/
override fun readFrom(range: LongRange): ByteReadChannel
}
/**
* Create LocalFileContent from base directory and relative path
*/
fun LocalFileContent(
baseDir: File,
relativePath: String,
contentType: ContentType = ContentType.defaultForFilePath(relativePath)
): LocalFileContent
/**
* Get default content type for file extension
*/
fun ContentType.Companion.defaultForFile(file: File): ContentType
fun ContentType.Companion.defaultForFilePath(path: String): ContentTypeTypes representing different parts of multipart content.
/**
* Base interface for multipart form data parts
*/
sealed class PartData {
abstract val name: String
abstract val headers: Headers
abstract fun dispose()
}
/**
* Text part data
*/
class PartData.FormItem(
override val name: String,
val value: String,
override val headers: Headers = Headers.Empty
) : PartData()
/**
* Binary part data
*/
class PartData.BinaryItem(
override val name: String,
val provider: ChannelProvider,
override val headers: Headers = Headers.Empty,
val filename: String? = null
) : PartData()
/**
* File part data
*/
class PartData.FileItem(
override val name: String,
val originalFileName: String?,
val provider: ChannelProvider,
override val headers: Headers = Headers.Empty
) : PartData()
/**
* Binary channel item
*/
class PartData.BinaryChannelItem(
override val name: String,
val provider: ChannelProvider,
override val headers: Headers = Headers.Empty
) : PartData()Content wrapper for monitoring upload/download progress.
/**
* Content wrapper that observes transfer progress
*/
class ObservableContent<T : OutgoingContent>(
private val delegate: T,
private val listener: (Long) -> Unit
) : OutgoingContent by delegate {
override suspend fun writeTo(channel: ByteWriteChannel) {
val observableChannel = channel.observable(listener)
delegate.writeTo(observableChannel)
}
}
/**
* Create observable content with progress callback
*/
fun <T : OutgoingContent> T.observable(
listener: (bytesWritten: Long) -> Unit
): ObservableContent<T>Content type definitions and serialization support.
/**
* Set content type for request
*/
fun HttpRequestBuilder.contentType(contentType: ContentType)
/**
* Set request body with automatic content type detection
*/
fun HttpRequestBuilder.setBody(body: Any)
/**
* Set request body with explicit type information
*/
fun HttpRequestBuilder.setBody(body: Any, typeInfo: TypeInfo)
/**
* Common content types
*/
object ContentType {
object Application {
val Json: ContentType // application/json
val Xml: ContentType // application/xml
val FormUrlEncoded: ContentType // application/x-www-form-urlencoded
val OctetStream: ContentType // application/octet-stream
val Pdf: ContentType // application/pdf
val Zip: ContentType // application/zip
val ProtoBuf: ContentType // application/x-protobuf
}
object Text {
val Plain: ContentType // text/plain
val Html: ContentType // text/html
val CSS: ContentType // text/css
val JavaScript: ContentType // text/javascript
val CSV: ContentType // text/csv
val EventStream: ContentType // text/event-stream
}
object Image {
val PNG: ContentType // image/png
val JPEG: ContentType // image/jpeg
val GIF: ContentType // image/gif
val SVG: ContentType // image/svg+xml
val WebP: ContentType // image/webp
}
object MultiPart {
val FormData: ContentType // multipart/form-data
val Mixed: ContentType // multipart/mixed
val Alternative: ContentType // multipart/alternative
val Related: ContentType // multipart/related
}
object Video {
val MP4: ContentType // video/mp4
val MPEG: ContentType // video/mpeg
val QuickTime: ContentType // video/quicktime
}
object Audio {
val MP3: ContentType // audio/mpeg
val MP4: ContentType // audio/mp4
val OGG: ContentType // audio/ogg
val WAV: ContentType // audio/wav
}
}Different types of outgoing content for HTTP requests.
/**
* Base class for all outgoing content
*/
abstract class OutgoingContent {
abstract val contentType: ContentType?
abstract val contentLength: Long?
/** Text content with string body */
abstract class TextContent(
private val text: String,
override val contentType: ContentType
) : OutgoingContent {
override val contentLength: Long = text.toByteArray().size.toLong()
}
/** Binary content with byte array body */
abstract class ByteArrayContent : OutgoingContent {
abstract fun bytes(): ByteArray
override val contentLength: Long? get() = bytes().size.toLong()
}
/** Content that can be read from a channel */
abstract class ReadChannelContent : OutgoingContent {
abstract fun readFrom(): ByteReadChannel
open fun readFrom(range: LongRange): ByteReadChannel = readFrom()
}
/** Content that writes to a channel */
abstract class WriteChannelContent(
override val contentType: ContentType? = null,
override val contentLength: Long? = null
) : OutgoingContent {
abstract suspend fun writeTo(channel: ByteWriteChannel)
}
/** No content (for methods like DELETE, GET) */
object NoContent : OutgoingContent {
override val contentLength: Long? = 0L
override val contentType: ContentType? = null
}
}Usage Examples:
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import io.ktor.utils.io.*
import java.io.File
val client = HttpClient()
// Submit URL-encoded form
val formResponse = client.submitForm(
url = "https://api.example.com/login",
formParameters = Parameters.build {
append("username", "john_doe")
append("password", "secret123")
append("remember", "true")
}
)
// Submit form as query parameters
val queryResponse = client.submitForm(
url = "https://api.example.com/search",
formParameters = Parameters.build {
append("q", "kotlin ktor")
append("limit", "10")
},
encodeInQuery = true
)
// Submit multipart form with binary data
val multipartResponse = client.submitFormWithBinaryData(
url = "https://api.example.com/upload",
formData = formData {
append("title", "My Document")
append("description", "Important file upload")
append("file", File("document.pdf").readBytes(), Headers.build {
append(HttpHeaders.ContentDisposition, "filename=\"document.pdf\"")
})
append("metadata", """{"category": "documents"}""")
}
)
// Manual form data construction
val manualFormData = formData {
// Text fields
append("name", "John Doe")
append("age", 30)
append("active", true)
// File upload (JVM)
val imageFile = File("profile.jpg")
append("avatar", imageFile, Headers.build {
append(HttpHeaders.ContentType, "image/jpeg")
})
// Binary data
val documentBytes = "Document content".toByteArray()
append("document", documentBytes, Headers.build {
append(HttpHeaders.ContentDisposition, "filename=\"document.txt\"")
}, filename = "document.txt")
// Streaming content
append("stream", {
// Return a ByteReadChannel
ByteReadChannel("Streaming data content")
}, filename = "stream.txt")
}
val manualResponse = client.post("https://api.example.com/complex-upload") {
setBody(MultiPartFormDataContent(manualFormData))
}
// File upload with progress monitoring
val largeFile = File("large-video.mp4")
val fileContent = LocalFileContent(largeFile).observable { bytesWritten ->
val progress = (bytesWritten.toDouble() / largeFile.length() * 100).toInt()
println("Upload progress: $progress%")
}
val uploadResponse = client.post("https://api.example.com/video-upload") {
setBody(fileContent)
}
// JSON content handling
val jsonResponse = client.post("https://api.example.com/users") {
contentType(ContentType.Application.Json)
setBody("""
{
"name": "Jane Smith",
"email": "jane@example.com",
"profile": {
"age": 28,
"location": "New York"
}
}
""".trimIndent())
}
// XML content handling
val xmlResponse = client.post("https://api.example.com/xml-endpoint") {
contentType(ContentType.Application.Xml)
setBody("""
<?xml version="1.0" encoding="UTF-8"?>
<user>
<name>Bob Johnson</name>
<email>bob@example.com</email>
</user>
""".trimIndent())
}
// Custom content with headers
val customResponse = client.post("https://api.example.com/custom") {
headers {
append("X-Custom-Header", "custom-value")
append("Authorization", "Bearer token123")
}
contentType(ContentType.Application.OctetStream)
setBody("Custom binary content".toByteArray())
}
// Streaming request content
val streamingResponse = client.post("https://api.example.com/stream") {
setBody(object : OutgoingContent.WriteChannelContent() {
override val contentType = ContentType.Text.Plain
override suspend fun writeTo(channel: ByteWriteChannel) {
repeat(1000) { i ->
channel.writeStringUtf8("Line $i\n")
if (i % 100 == 0) {
channel.flush()
kotlinx.coroutines.delay(10) // Simulate streaming delay
}
}
}
})
}
// Form data with content length
val contentLengthForm = FormDataContent(Parameters.build {
append("field1", "value1")
append("field2", "value2")
})
val contentLengthResponse = client.post("https://api.example.com/sized-form") {
setBody(contentLengthForm)
headers {
append(HttpHeaders.ContentLength, contentLengthForm.contentLength.toString())
}
}
client.close()Parameter building and manipulation for forms and URLs.
/**
* Immutable parameters collection
*/
interface Parameters : StringValues {
companion object {
val Empty: Parameters
/** Build parameters using DSL */
fun build(builder: ParametersBuilder.() -> Unit): Parameters
}
}
/**
* Mutable parameters builder
*/
class ParametersBuilder : StringValuesBuilder() {
/** Build immutable Parameters */
fun build(): Parameters
}
/**
* Common string values interface
*/
interface StringValues {
/** Get all value names */
fun names(): Set<String>
/** Get all values for a name */
fun getAll(name: String): List<String>?
/** Get first value for a name */
operator fun get(name: String): String?
/** Check if name exists */
fun contains(name: String): Boolean
/** Check if name contains specific value */
fun contains(name: String, value: String): Boolean
/** Iterate over all entries */
fun forEach(action: (String, List<String>) -> Unit)
}