or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

builtin-plugins.mdcaching.mdcookies.mdengine-configuration.mdforms.mdhttp-client.mdindex.mdplugin-system.mdrequest-building.mdresponse-handling.mdresponse-observation.mdutilities.mdwebsockets.md

forms.mddocs/

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