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)
}