Ktor HTTP Client Core for tvOS ARM64 - multiplatform asynchronous HTTP client library with coroutines support
—
The Ktor HTTP Client Core provides comprehensive form data construction and submission utilities with support for URL-encoded forms, multipart forms, and file uploads using type-safe DSL builders. This enables easy handling of HTML forms and file uploads with proper content type management.
Content class for URL-encoded form data submission (application/x-www-form-urlencoded).
class FormDataContent(
private val formData: List<Pair<String, String>>
) : OutgoingContent {
constructor(formData: Parameters) : this(formData.flattenEntries())
constructor(vararg formData: Pair<String, String>) : this(formData.toList())
override val contentType: ContentType = ContentType.Application.FormUrlEncoded
override val contentLength: Long? get() = formData.formUrlEncode().toByteArray().size.toLong()
}Content class for multipart form data submission (multipart/form-data) supporting both text fields and file uploads.
class MultiPartFormDataContent(
private val parts: List<PartData>,
private val boundary: String = generateBoundary(),
override val contentType: ContentType = ContentType.MultiPart.FormData.withParameter("boundary", boundary)
) : OutgoingContent {
override val contentLength: Long? = null // Streamed content
companion object {
fun generateBoundary(): String
}
}sealed class PartData : Closeable {
abstract val dispose: () -> Unit
abstract val headers: Headers
abstract val name: String?
override fun close() = dispose()
}// Text form field
data class PartData.FormItem(
val value: String,
override val dispose: () -> Unit = {},
override val headers: Headers = Headers.Empty
) : PartData() {
override val name: String? get() = headers[HttpHeaders.ContentDisposition]?.let {
ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.Name)
}
}
// Binary file upload
data class PartData.FileItem(
val provider: () -> Input,
override val dispose: () -> Unit = {},
override val headers: Headers = Headers.Empty
) : PartData() {
override val name: String? get() = headers[HttpHeaders.ContentDisposition]?.let {
ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.Name)
}
val originalFileName: String? get() = headers[HttpHeaders.ContentDisposition]?.let {
ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.FileName)
}
}
// Binary data with custom provider
data class PartData.BinaryItem(
val provider: () -> ByteReadPacket,
override val dispose: () -> Unit = {},
override val headers: Headers = Headers.Empty,
val contentLength: Long? = null
) : PartData() {
override val name: String? get() = headers[HttpHeaders.ContentDisposition]?.let {
ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.Name)
}
}
// Binary channel data
data class PartData.BinaryChannelItem(
val provider: () -> ByteReadChannel,
override val dispose: () -> Unit = {},
override val headers: Headers = Headers.Empty,
val contentLength: Long? = null
) : PartData() {
override val name: String? get() = headers[HttpHeaders.ContentDisposition]?.let {
ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.Name)
}
}Type-safe DSL builder for constructing multipart form data with fluent API.
class FormBuilder {
private val parts = mutableListOf<PartData>()
// Add text field
fun append(key: String, value: String, headers: Headers = Headers.Empty) {
val partHeaders = headers + headersOf(
HttpHeaders.ContentDisposition to "form-data; name=\"$key\""
)
parts.add(PartData.FormItem(value, headers = partHeaders))
}
// Add file upload with content provider
fun append(
key: String,
filename: String,
contentType: ContentType? = null,
size: Long? = null,
headers: Headers = Headers.Empty,
block: suspend ByteWriteChannel.() -> Unit
) {
val partHeaders = buildHeaders(headers) {
append(HttpHeaders.ContentDisposition, "form-data; name=\"$key\"; filename=\"$filename\"")
contentType?.let { append(HttpHeaders.ContentType, it.toString()) }
}
parts.add(PartData.BinaryChannelItem(
provider = {
GlobalScope.writer(coroutineContext) {
block()
}.channel
},
headers = partHeaders,
contentLength = size
))
}
// Add binary data with input provider
fun appendInput(
key: String,
headers: Headers = Headers.Empty,
size: Long? = null,
block: suspend ByteWriteChannel.() -> Unit
) {
val partHeaders = buildHeaders(headers) {
append(HttpHeaders.ContentDisposition, "form-data; name=\"$key\"")
}
parts.add(PartData.BinaryChannelItem(
provider = {
GlobalScope.writer(coroutineContext) {
block()
}.channel
},
headers = partHeaders,
contentLength = size
))
}
// Add existing PartData
fun append(part: PartData) {
parts.add(part)
}
fun build(): List<PartData> = parts.toList()
}// Submit URL-encoded form
suspend fun HttpClient.submitForm(
url: String,
formParameters: Parameters = Parameters.Empty,
encodeInQuery: Boolean = false,
block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse
suspend fun HttpClient.submitForm(
url: Url,
formParameters: Parameters = Parameters.Empty,
encodeInQuery: Boolean = false,
block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse
// Submit multipart form with binary data
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// Build form data using DSL
fun formData(block: FormBuilder.() -> Unit): List<PartData> {
return FormBuilder().apply(block).build()
}
// Create multipart content from parts
fun MultiPartFormDataContent(
parts: List<PartData>,
boundary: String = generateBoundary(),
contentType: ContentType = ContentType.MultiPart.FormData.withParameter("boundary", boundary)
): MultiPartFormDataContent
// Create multipart content using DSL
fun MultiPartFormDataContent(
boundary: String = generateBoundary(),
block: FormBuilder.() -> Unit
): MultiPartFormDataContent {
val parts = FormBuilder().apply(block).build()
return MultiPartFormDataContent(parts, boundary)
}val client = HttpClient()
// Simple form submission
val response = client.submitForm(
url = "https://httpbin.org/post",
formParameters = parametersOf(
"username" to listOf("john_doe"),
"password" to listOf("secret123"),
"remember" to listOf("true")
)
)
// Form submission with additional configuration
val response2 = client.submitForm(
url = "https://api.example.com/login",
formParameters = parametersOf(
"email" to listOf("user@example.com"),
"password" to listOf("password")
)
) {
header("X-Client-Version", "1.0")
header("Accept", "application/json")
}
client.close()val client = HttpClient()
// File upload using submitFormWithBinaryData
val formData = formData {
append("username", "alice")
append("description", "Profile picture upload")
// Upload file from ByteArray
append(
key = "avatar",
filename = "profile.jpg",
contentType = ContentType.Image.JPEG
) {
val imageBytes = File("profile.jpg").readBytes()
writeFully(imageBytes)
}
}
val response = client.submitFormWithBinaryData(
url = "https://api.example.com/upload",
formData = formData
) {
header("Authorization", "Bearer $accessToken")
}
client.close()val client = HttpClient()
// Create form content manually
val formContent = MultiPartFormDataContent(
parts = formData {
append("title", "My Document")
append("category", "reports")
append(
key = "document",
filename = "report.pdf",
contentType = ContentType.Application.Pdf,
size = 1024000
) {
// Stream file content
File("report.pdf").inputStream().use { input ->
input.copyTo(this)
}
}
}
)
val response = client.post("https://documents.example.com/upload") {
setBody(formContent)
header("X-Upload-Source", "mobile-app")
}fun buildDynamicForm(fields: Map<String, Any>, files: List<FileUpload>): List<PartData> {
return formData {
// Add text fields
fields.forEach { (key, value) ->
append(key, value.toString())
}
// Add file uploads
files.forEach { fileUpload ->
append(
key = fileUpload.fieldName,
filename = fileUpload.originalName,
contentType = ContentType.parse(fileUpload.mimeType),
size = fileUpload.size
) {
fileUpload.inputStream.copyTo(this)
}
}
}
}
data class FileUpload(
val fieldName: String,
val originalName: String,
val mimeType: String,
val size: Long,
val inputStream: InputStream
)val client = HttpClient {
install(BodyProgress) {
onUpload { bytesSentTotal, contentLength ->
val progress = contentLength?.let {
(bytesSentTotal.toDouble() / it * 100).roundToInt()
} ?: 0
println("Upload progress: $progress% ($bytesSentTotal bytes)")
}
}
}
val largeFormData = formData {
append("description", "Large file upload")
append(
key = "large_file",
filename = "large_video.mp4",
contentType = ContentType.Video.MP4,
size = 100 * 1024 * 1024 // 100MB
) {
// Stream large file with progress tracking
File("large_video.mp4").inputStream().use { input ->
input.copyTo(this)
}
}
}
val response = client.submitFormWithBinaryData(
"https://media.example.com/upload",
largeFormData
)val formData = formData {
// Text field with custom headers
append("metadata", """{"version": 1, "format": "json"}""")
// JSON file upload
append(
key = "config",
filename = "config.json",
contentType = ContentType.Application.Json
) {
val jsonConfig = """
{
"settings": {
"theme": "dark",
"notifications": true
}
}
""".trimIndent()
writeStringUtf8(jsonConfig)
}
// Binary data with custom content type
append(
key = "binary_data",
filename = "data.bin",
contentType = ContentType.Application.OctetStream
) {
// Write binary data
repeat(1000) {
writeByte(it.toByte())
}
}
}suspend fun uploadFormWithValidation(
client: HttpClient,
formData: List<PartData>
): Result<String> {
return try {
// Validate form data
validateFormData(formData)
val response = client.submitFormWithBinaryData(
"https://api.example.com/upload",
formData
) {
timeout {
requestTimeoutMillis = 300000 // 5 minutes for large uploads
}
}
when (response.status) {
HttpStatusCode.OK -> Result.success(response.bodyAsText())
HttpStatusCode.BadRequest -> {
val error = response.bodyAsText()
Result.failure(IllegalArgumentException("Validation failed: $error"))
}
HttpStatusCode.RequestEntityTooLarge -> {
Result.failure(IllegalArgumentException("File too large"))
}
else -> {
Result.failure(Exception("Upload failed with status: ${response.status}"))
}
}
} catch (e: Exception) {
Result.failure(e)
}
}
private fun validateFormData(formData: List<PartData>) {
formData.forEach { part ->
when (part) {
is PartData.FileItem -> {
// Validate file uploads
val contentType = part.headers[HttpHeaders.ContentType]
if (contentType != null && !isAllowedContentType(contentType)) {
throw IllegalArgumentException("Unsupported content type: $contentType")
}
}
is PartData.FormItem -> {
// Validate text fields
if (part.value.length > 1000) {
throw IllegalArgumentException("Text field too long: ${part.name}")
}
}
else -> { /* other validations */ }
}
}
}
private fun isAllowedContentType(contentType: String): Boolean {
val allowed = listOf(
"image/jpeg", "image/png", "image/gif",
"application/pdf", "text/plain"
)
return allowed.any { contentType.startsWith(it) }
}suspend fun uploadMultipleFiles(
client: HttpClient,
files: List<File>,
metadata: Map<String, String>
): HttpResponse {
val formData = formData {
// Add metadata fields
metadata.forEach { (key, value) ->
append(key, value)
}
// Add multiple files
files.forEachIndexed { index, file ->
append(
key = "file_$index",
filename = file.name,
contentType = ContentType.defaultForFile(file),
size = file.length()
) {
file.inputStream().use { input ->
input.copyTo(this)
}
}
}
}
return client.submitFormWithBinaryData(
"https://api.example.com/batch-upload",
formData
)
}suspend fun uploadFileInChunks(
client: HttpClient,
file: File,
chunkSize: Int = 1024 * 1024 // 1MB chunks
): List<HttpResponse> {
val responses = mutableListOf<HttpResponse>()
val totalChunks = (file.length() + chunkSize - 1) / chunkSize
file.inputStream().use { input ->
repeat(totalChunks.toInt()) { chunkIndex ->
val buffer = ByteArray(chunkSize)
val bytesRead = input.read(buffer)
val chunkData = buffer.copyOf(bytesRead)
val chunkFormData = formData {
append("chunk_index", chunkIndex.toString())
append("total_chunks", totalChunks.toString())
append("filename", file.name)
append(
key = "chunk",
filename = "${file.name}.part$chunkIndex",
contentType = ContentType.Application.OctetStream,
size = bytesRead.toLong()
) {
writeFully(chunkData)
}
}
val response = client.submitFormWithBinaryData(
"https://api.example.com/upload-chunk",
chunkFormData
)
responses.add(response)
}
}
return responses
}suspend fun uploadFromStream(
client: HttpClient,
inputStream: InputStream,
filename: String,
contentType: ContentType,
metadata: Map<String, String> = emptyMap()
): HttpResponse {
val formData = formData {
// Add metadata
metadata.forEach { (key, value) ->
append(key, value)
}
// Add streamed file
append(
key = "file",
filename = filename,
contentType = contentType
) {
inputStream.use { input ->
input.copyTo(this)
}
}
}
return client.submitFormWithBinaryData(
"https://api.example.com/stream-upload",
formData
)
}Install with Tessl CLI
npx tessl i tessl/maven-io-ktor--ktor-client-core-tvosarm64