0
# Form Handling
1
2
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.
3
4
## Core Form API
5
6
### FormDataContent
7
8
Content class for URL-encoded form data submission (application/x-www-form-urlencoded).
9
10
```kotlin { .api }
11
class FormDataContent(
12
private val formData: List<Pair<String, String>>
13
) : OutgoingContent {
14
constructor(formData: Parameters) : this(formData.flattenEntries())
15
constructor(vararg formData: Pair<String, String>) : this(formData.toList())
16
17
override val contentType: ContentType = ContentType.Application.FormUrlEncoded
18
override val contentLength: Long? get() = formData.formUrlEncode().toByteArray().size.toLong()
19
}
20
```
21
22
### MultiPartFormDataContent
23
24
Content class for multipart form data submission (multipart/form-data) supporting both text fields and file uploads.
25
26
```kotlin { .api }
27
class MultiPartFormDataContent(
28
private val parts: List<PartData>,
29
private val boundary: String = generateBoundary(),
30
override val contentType: ContentType = ContentType.MultiPart.FormData.withParameter("boundary", boundary)
31
) : OutgoingContent {
32
33
override val contentLength: Long? = null // Streamed content
34
35
companion object {
36
fun generateBoundary(): String
37
}
38
}
39
```
40
41
## PartData Hierarchy
42
43
### Base PartData Interface
44
45
```kotlin { .api }
46
sealed class PartData : Closeable {
47
abstract val dispose: () -> Unit
48
abstract val headers: Headers
49
abstract val name: String?
50
51
override fun close() = dispose()
52
}
53
```
54
55
### PartData Implementations
56
57
```kotlin { .api }
58
// Text form field
59
data class PartData.FormItem(
60
val value: String,
61
override val dispose: () -> Unit = {},
62
override val headers: Headers = Headers.Empty
63
) : PartData() {
64
override val name: String? get() = headers[HttpHeaders.ContentDisposition]?.let {
65
ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.Name)
66
}
67
}
68
69
// Binary file upload
70
data class PartData.FileItem(
71
val provider: () -> Input,
72
override val dispose: () -> Unit = {},
73
override val headers: Headers = Headers.Empty
74
) : PartData() {
75
override val name: String? get() = headers[HttpHeaders.ContentDisposition]?.let {
76
ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.Name)
77
}
78
79
val originalFileName: String? get() = headers[HttpHeaders.ContentDisposition]?.let {
80
ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.FileName)
81
}
82
}
83
84
// Binary data with custom provider
85
data class PartData.BinaryItem(
86
val provider: () -> ByteReadPacket,
87
override val dispose: () -> Unit = {},
88
override val headers: Headers = Headers.Empty,
89
val contentLength: Long? = null
90
) : PartData() {
91
override val name: String? get() = headers[HttpHeaders.ContentDisposition]?.let {
92
ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.Name)
93
}
94
}
95
96
// Binary channel data
97
data class PartData.BinaryChannelItem(
98
val provider: () -> ByteReadChannel,
99
override val dispose: () -> Unit = {},
100
override val headers: Headers = Headers.Empty,
101
val contentLength: Long? = null
102
) : PartData() {
103
override val name: String? get() = headers[HttpHeaders.ContentDisposition]?.let {
104
ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.Name)
105
}
106
}
107
```
108
109
## Form Builder DSL
110
111
### FormBuilder Class
112
113
Type-safe DSL builder for constructing multipart form data with fluent API.
114
115
```kotlin { .api }
116
class FormBuilder {
117
private val parts = mutableListOf<PartData>()
118
119
// Add text field
120
fun append(key: String, value: String, headers: Headers = Headers.Empty) {
121
val partHeaders = headers + headersOf(
122
HttpHeaders.ContentDisposition to "form-data; name=\"$key\""
123
)
124
parts.add(PartData.FormItem(value, headers = partHeaders))
125
}
126
127
// Add file upload with content provider
128
fun append(
129
key: String,
130
filename: String,
131
contentType: ContentType? = null,
132
size: Long? = null,
133
headers: Headers = Headers.Empty,
134
block: suspend ByteWriteChannel.() -> Unit
135
) {
136
val partHeaders = buildHeaders(headers) {
137
append(HttpHeaders.ContentDisposition, "form-data; name=\"$key\"; filename=\"$filename\"")
138
contentType?.let { append(HttpHeaders.ContentType, it.toString()) }
139
}
140
141
parts.add(PartData.BinaryChannelItem(
142
provider = {
143
GlobalScope.writer(coroutineContext) {
144
block()
145
}.channel
146
},
147
headers = partHeaders,
148
contentLength = size
149
))
150
}
151
152
// Add binary data with input provider
153
fun appendInput(
154
key: String,
155
headers: Headers = Headers.Empty,
156
size: Long? = null,
157
block: suspend ByteWriteChannel.() -> Unit
158
) {
159
val partHeaders = buildHeaders(headers) {
160
append(HttpHeaders.ContentDisposition, "form-data; name=\"$key\"")
161
}
162
163
parts.add(PartData.BinaryChannelItem(
164
provider = {
165
GlobalScope.writer(coroutineContext) {
166
block()
167
}.channel
168
},
169
headers = partHeaders,
170
contentLength = size
171
))
172
}
173
174
// Add existing PartData
175
fun append(part: PartData) {
176
parts.add(part)
177
}
178
179
fun build(): List<PartData> = parts.toList()
180
}
181
```
182
183
## Form Submission Functions
184
185
### Simple Form Submission
186
187
```kotlin { .api }
188
// Submit URL-encoded form
189
suspend fun HttpClient.submitForm(
190
url: String,
191
formParameters: Parameters = Parameters.Empty,
192
encodeInQuery: Boolean = false,
193
block: HttpRequestBuilder.() -> Unit = {}
194
): HttpResponse
195
196
suspend fun HttpClient.submitForm(
197
url: Url,
198
formParameters: Parameters = Parameters.Empty,
199
encodeInQuery: Boolean = false,
200
block: HttpRequestBuilder.() -> Unit = {}
201
): HttpResponse
202
203
// Submit multipart form with binary data
204
suspend fun HttpClient.submitFormWithBinaryData(
205
url: String,
206
formData: List<PartData>,
207
block: HttpRequestBuilder.() -> Unit = {}
208
): HttpResponse
209
210
suspend fun HttpClient.submitFormWithBinaryData(
211
url: Url,
212
formData: List<PartData>,
213
block: HttpRequestBuilder.() -> Unit = {}
214
): HttpResponse
215
```
216
217
### Form Data Construction Functions
218
219
```kotlin { .api }
220
// Build form data using DSL
221
fun formData(block: FormBuilder.() -> Unit): List<PartData> {
222
return FormBuilder().apply(block).build()
223
}
224
225
// Create multipart content from parts
226
fun MultiPartFormDataContent(
227
parts: List<PartData>,
228
boundary: String = generateBoundary(),
229
contentType: ContentType = ContentType.MultiPart.FormData.withParameter("boundary", boundary)
230
): MultiPartFormDataContent
231
232
// Create multipart content using DSL
233
fun MultiPartFormDataContent(
234
boundary: String = generateBoundary(),
235
block: FormBuilder.() -> Unit
236
): MultiPartFormDataContent {
237
val parts = FormBuilder().apply(block).build()
238
return MultiPartFormDataContent(parts, boundary)
239
}
240
```
241
242
## Basic Usage
243
244
### URL-Encoded Form Submission
245
246
```kotlin
247
val client = HttpClient()
248
249
// Simple form submission
250
val response = client.submitForm(
251
url = "https://httpbin.org/post",
252
formParameters = parametersOf(
253
"username" to listOf("john_doe"),
254
"password" to listOf("secret123"),
255
"remember" to listOf("true")
256
)
257
)
258
259
// Form submission with additional configuration
260
val response2 = client.submitForm(
261
url = "https://api.example.com/login",
262
formParameters = parametersOf(
263
"email" to listOf("user@example.com"),
264
"password" to listOf("password")
265
)
266
) {
267
header("X-Client-Version", "1.0")
268
header("Accept", "application/json")
269
}
270
271
client.close()
272
```
273
274
### Multipart Form with File Upload
275
276
```kotlin
277
val client = HttpClient()
278
279
// File upload using submitFormWithBinaryData
280
val formData = formData {
281
append("username", "alice")
282
append("description", "Profile picture upload")
283
284
// Upload file from ByteArray
285
append(
286
key = "avatar",
287
filename = "profile.jpg",
288
contentType = ContentType.Image.JPEG
289
) {
290
val imageBytes = File("profile.jpg").readBytes()
291
writeFully(imageBytes)
292
}
293
}
294
295
val response = client.submitFormWithBinaryData(
296
url = "https://api.example.com/upload",
297
formData = formData
298
) {
299
header("Authorization", "Bearer $accessToken")
300
}
301
302
client.close()
303
```
304
305
### Manual Form Content Creation
306
307
```kotlin
308
val client = HttpClient()
309
310
// Create form content manually
311
val formContent = MultiPartFormDataContent(
312
parts = formData {
313
append("title", "My Document")
314
append("category", "reports")
315
316
append(
317
key = "document",
318
filename = "report.pdf",
319
contentType = ContentType.Application.Pdf,
320
size = 1024000
321
) {
322
// Stream file content
323
File("report.pdf").inputStream().use { input ->
324
input.copyTo(this)
325
}
326
}
327
}
328
)
329
330
val response = client.post("https://documents.example.com/upload") {
331
setBody(formContent)
332
header("X-Upload-Source", "mobile-app")
333
}
334
```
335
336
## Advanced Form Features
337
338
### Dynamic Form Building
339
340
```kotlin
341
fun buildDynamicForm(fields: Map<String, Any>, files: List<FileUpload>): List<PartData> {
342
return formData {
343
// Add text fields
344
fields.forEach { (key, value) ->
345
append(key, value.toString())
346
}
347
348
// Add file uploads
349
files.forEach { fileUpload ->
350
append(
351
key = fileUpload.fieldName,
352
filename = fileUpload.originalName,
353
contentType = ContentType.parse(fileUpload.mimeType),
354
size = fileUpload.size
355
) {
356
fileUpload.inputStream.copyTo(this)
357
}
358
}
359
}
360
}
361
362
data class FileUpload(
363
val fieldName: String,
364
val originalName: String,
365
val mimeType: String,
366
val size: Long,
367
val inputStream: InputStream
368
)
369
```
370
371
### Progress Tracking for Uploads
372
373
```kotlin
374
val client = HttpClient {
375
install(BodyProgress) {
376
onUpload { bytesSentTotal, contentLength ->
377
val progress = contentLength?.let {
378
(bytesSentTotal.toDouble() / it * 100).roundToInt()
379
} ?: 0
380
println("Upload progress: $progress% ($bytesSentTotal bytes)")
381
}
382
}
383
}
384
385
val largeFormData = formData {
386
append("description", "Large file upload")
387
388
append(
389
key = "large_file",
390
filename = "large_video.mp4",
391
contentType = ContentType.Video.MP4,
392
size = 100 * 1024 * 1024 // 100MB
393
) {
394
// Stream large file with progress tracking
395
File("large_video.mp4").inputStream().use { input ->
396
input.copyTo(this)
397
}
398
}
399
}
400
401
val response = client.submitFormWithBinaryData(
402
"https://media.example.com/upload",
403
largeFormData
404
)
405
```
406
407
### Custom Content Types and Headers
408
409
```kotlin
410
val formData = formData {
411
// Text field with custom headers
412
append("metadata", """{"version": 1, "format": "json"}""")
413
414
// JSON file upload
415
append(
416
key = "config",
417
filename = "config.json",
418
contentType = ContentType.Application.Json
419
) {
420
val jsonConfig = """
421
{
422
"settings": {
423
"theme": "dark",
424
"notifications": true
425
}
426
}
427
""".trimIndent()
428
writeStringUtf8(jsonConfig)
429
}
430
431
// Binary data with custom content type
432
append(
433
key = "binary_data",
434
filename = "data.bin",
435
contentType = ContentType.Application.OctetStream
436
) {
437
// Write binary data
438
repeat(1000) {
439
writeByte(it.toByte())
440
}
441
}
442
}
443
```
444
445
### Form Validation and Error Handling
446
447
```kotlin
448
suspend fun uploadFormWithValidation(
449
client: HttpClient,
450
formData: List<PartData>
451
): Result<String> {
452
return try {
453
// Validate form data
454
validateFormData(formData)
455
456
val response = client.submitFormWithBinaryData(
457
"https://api.example.com/upload",
458
formData
459
) {
460
timeout {
461
requestTimeoutMillis = 300000 // 5 minutes for large uploads
462
}
463
}
464
465
when (response.status) {
466
HttpStatusCode.OK -> Result.success(response.bodyAsText())
467
HttpStatusCode.BadRequest -> {
468
val error = response.bodyAsText()
469
Result.failure(IllegalArgumentException("Validation failed: $error"))
470
}
471
HttpStatusCode.RequestEntityTooLarge -> {
472
Result.failure(IllegalArgumentException("File too large"))
473
}
474
else -> {
475
Result.failure(Exception("Upload failed with status: ${response.status}"))
476
}
477
}
478
} catch (e: Exception) {
479
Result.failure(e)
480
}
481
}
482
483
private fun validateFormData(formData: List<PartData>) {
484
formData.forEach { part ->
485
when (part) {
486
is PartData.FileItem -> {
487
// Validate file uploads
488
val contentType = part.headers[HttpHeaders.ContentType]
489
if (contentType != null && !isAllowedContentType(contentType)) {
490
throw IllegalArgumentException("Unsupported content type: $contentType")
491
}
492
}
493
is PartData.FormItem -> {
494
// Validate text fields
495
if (part.value.length > 1000) {
496
throw IllegalArgumentException("Text field too long: ${part.name}")
497
}
498
}
499
else -> { /* other validations */ }
500
}
501
}
502
}
503
504
private fun isAllowedContentType(contentType: String): Boolean {
505
val allowed = listOf(
506
"image/jpeg", "image/png", "image/gif",
507
"application/pdf", "text/plain"
508
)
509
return allowed.any { contentType.startsWith(it) }
510
}
511
```
512
513
## File Upload Patterns
514
515
### Multiple File Upload
516
517
```kotlin
518
suspend fun uploadMultipleFiles(
519
client: HttpClient,
520
files: List<File>,
521
metadata: Map<String, String>
522
): HttpResponse {
523
524
val formData = formData {
525
// Add metadata fields
526
metadata.forEach { (key, value) ->
527
append(key, value)
528
}
529
530
// Add multiple files
531
files.forEachIndexed { index, file ->
532
append(
533
key = "file_$index",
534
filename = file.name,
535
contentType = ContentType.defaultForFile(file),
536
size = file.length()
537
) {
538
file.inputStream().use { input ->
539
input.copyTo(this)
540
}
541
}
542
}
543
}
544
545
return client.submitFormWithBinaryData(
546
"https://api.example.com/batch-upload",
547
formData
548
)
549
}
550
```
551
552
### Chunked File Upload
553
554
```kotlin
555
suspend fun uploadFileInChunks(
556
client: HttpClient,
557
file: File,
558
chunkSize: Int = 1024 * 1024 // 1MB chunks
559
): List<HttpResponse> {
560
561
val responses = mutableListOf<HttpResponse>()
562
val totalChunks = (file.length() + chunkSize - 1) / chunkSize
563
564
file.inputStream().use { input ->
565
repeat(totalChunks.toInt()) { chunkIndex ->
566
val buffer = ByteArray(chunkSize)
567
val bytesRead = input.read(buffer)
568
val chunkData = buffer.copyOf(bytesRead)
569
570
val chunkFormData = formData {
571
append("chunk_index", chunkIndex.toString())
572
append("total_chunks", totalChunks.toString())
573
append("filename", file.name)
574
575
append(
576
key = "chunk",
577
filename = "${file.name}.part$chunkIndex",
578
contentType = ContentType.Application.OctetStream,
579
size = bytesRead.toLong()
580
) {
581
writeFully(chunkData)
582
}
583
}
584
585
val response = client.submitFormWithBinaryData(
586
"https://api.example.com/upload-chunk",
587
chunkFormData
588
)
589
590
responses.add(response)
591
}
592
}
593
594
return responses
595
}
596
```
597
598
### Stream-Based Upload
599
600
```kotlin
601
suspend fun uploadFromStream(
602
client: HttpClient,
603
inputStream: InputStream,
604
filename: String,
605
contentType: ContentType,
606
metadata: Map<String, String> = emptyMap()
607
): HttpResponse {
608
609
val formData = formData {
610
// Add metadata
611
metadata.forEach { (key, value) ->
612
append(key, value)
613
}
614
615
// Add streamed file
616
append(
617
key = "file",
618
filename = filename,
619
contentType = contentType
620
) {
621
inputStream.use { input ->
622
input.copyTo(this)
623
}
624
}
625
}
626
627
return client.submitFormWithBinaryData(
628
"https://api.example.com/stream-upload",
629
formData
630
)
631
}
632
```
633
634
## Best Practices
635
636
1. **Content Type Detection**: Always specify appropriate content types for file uploads
637
2. **File Size Validation**: Validate file sizes before uploading to prevent server errors
638
3. **Progress Tracking**: Use BodyProgress plugin for large file uploads to provide user feedback
639
4. **Error Handling**: Implement proper error handling for network failures and server errors
640
5. **Memory Management**: Use streaming for large files to avoid loading entire files into memory
641
6. **Timeout Configuration**: Set appropriate timeouts for large uploads
642
7. **Chunked Uploads**: Consider chunked uploads for very large files to improve reliability
643
8. **Security**: Validate file types and sanitize filenames to prevent security issues
644
9. **Cleanup**: Always close InputStreams and dispose of resources properly
645
10. **Rate Limiting**: Be aware of server-side rate limits and implement retry logic if necessary
646
11. **Boundary Generation**: Use unique boundaries for multipart forms to avoid conflicts
647
12. **Character Encoding**: Ensure proper character encoding for text fields in forms