CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-ktor--ktor-client-core-tvosarm64

Ktor HTTP Client Core for tvOS ARM64 - multiplatform asynchronous HTTP client library with coroutines support

Pending
Overview
Eval results
Files

forms.mddocs/

Form Handling

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.

Core Form API

FormDataContent

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

MultiPartFormDataContent

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

PartData Hierarchy

Base PartData Interface

sealed class PartData : Closeable {
    abstract val dispose: () -> Unit
    abstract val headers: Headers
    abstract val name: String?
    
    override fun close() = dispose()
}

PartData Implementations

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

Form Builder DSL

FormBuilder Class

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

Form Submission Functions

Simple Form Submission

// 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

Form Data Construction Functions

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

Basic Usage

URL-Encoded Form Submission

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

Multipart Form with File Upload

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

Manual Form Content Creation

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

Advanced Form Features

Dynamic Form Building

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
)

Progress Tracking for Uploads

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
)

Custom Content Types and Headers

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

Form Validation and Error Handling

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

File Upload Patterns

Multiple File Upload

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

Chunked File Upload

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
}

Stream-Based Upload

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

Best Practices

  1. Content Type Detection: Always specify appropriate content types for file uploads
  2. File Size Validation: Validate file sizes before uploading to prevent server errors
  3. Progress Tracking: Use BodyProgress plugin for large file uploads to provide user feedback
  4. Error Handling: Implement proper error handling for network failures and server errors
  5. Memory Management: Use streaming for large files to avoid loading entire files into memory
  6. Timeout Configuration: Set appropriate timeouts for large uploads
  7. Chunked Uploads: Consider chunked uploads for very large files to improve reliability
  8. Security: Validate file types and sanitize filenames to prevent security issues
  9. Cleanup: Always close InputStreams and dispose of resources properly
  10. Rate Limiting: Be aware of server-side rate limits and implement retry logic if necessary
  11. Boundary Generation: Use unique boundaries for multipart forms to avoid conflicts
  12. Character Encoding: Ensure proper character encoding for text fields in forms

Install with Tessl CLI

npx tessl i tessl/maven-io-ktor--ktor-client-core-tvosarm64

docs

builtin-plugins.md

caching.md

cookies.md

engine-configuration.md

forms.md

http-client.md

index.md

plugin-system.md

request-building.md

response-handling.md

response-observation.md

utilities.md

websockets.md

tile.json