Skip to content

Conversation

@osipxd
Copy link
Member

@osipxd osipxd commented Nov 12, 2025

Subsystem
ktor-client-core

Motivation
KTOR-9037 Multipart/form-data: Make formData's block inline

Solution
Add the inline modifier to the function

@osipxd osipxd self-assigned this Nov 12, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 12, 2025

Walkthrough

Added a public no-arg FormBuilder() constructor and build() method, many append(...) overloads and appendInput, a new InputProvider type, builder-style formData entry, and widened FormDataContent surface (constructor, properties, bytes()); annotated builder members with @PublishedApi for inline use.

Changes

Cohort / File(s) Summary
Public API files
ktor-client/ktor-client-core/api/ktor-client-core.api, ktor-client/ktor-client-core/api/ktor-client-core.klib.api
Added public FormBuilder() constructor and build(): List<FormPart<*>>. Added many append(...) overloads (InputProvider, Iterable, Array, Boolean, ByteArray, Number, String, Source) and appendInput(...). Added InputProvider class. Added formData(Function1<FormBuilder, Unit>): List<PartData> builder entry. Expanded FormDataContent API: new constructor accepting Parameters, exposed contentLength, contentType, formData, and added bytes().
Source implementation
ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt
Made formData inline and added Kotlin contracts. Annotated FormBuilder and its internal constructor and build() with @PublishedApi. Replaced broad kotlinx.io import with explicit Sink/Source imports. Updated copyright year.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Verify correct use and placement of @PublishedApi relative to the inline formData function.
  • Confirm consistency and default Headers parameters across new append overloads and appendInput.
  • Check binary/API compatibility impact of making FormBuilder() public and exposing FormDataContent members and bytes().
  • Inspect InputProvider for lifecycle/streaming semantics and nullability of size.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically refers to the main change: making the formData function an inline function, which aligns with the primary objective stated in the PR.
Description check ✅ Passed The description follows the required template with Subsystem, Motivation, and Solution sections completed. All required information is present and directly addresses the PR objective.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch osipxd/make-formData-inline

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@osipxd osipxd force-pushed the osipxd/make-formData-inline branch from c07f996 to 2b93efe Compare November 13, 2025 09:57
@osipxd osipxd marked this pull request as ready for review November 13, 2025 09:57
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt (1)

89-90: Document the builder contract on the inline overload.

We invoke block exactly once here. Adding the same ExperimentalContracts opt-in + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } used elsewhere tightens the DSL guarantees and keeps the new inline overload aligned with the rest of the API surface.

- public inline fun formData(block: FormBuilder.() -> Unit): List<PartData> =
-    formData(*FormBuilder().apply(block).build().toTypedArray())
+@OptIn(ExperimentalContracts::class)
+public inline fun formData(block: FormBuilder.() -> Unit): List<PartData> {
+    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
+    return formData(*FormBuilder().apply(block).build().toTypedArray())
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b293fc and 2b93efe.

📒 Files selected for processing (3)
  • ktor-client/ktor-client-core/api/ktor-client-core.api (2 hunks)
  • ktor-client/ktor-client-core/api/ktor-client-core.klib.api (3 hunks)
  • ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-14T18:05:02.321Z
Learnt from: bjhham
Repo: ktorio/ktor PR: 4855
File: ktor-server/ktor-server-plugins/ktor-server-di/api/ktor-server-di.klib.api:334-336
Timestamp: 2025-05-14T18:05:02.321Z
Learning: Breaking changes in constructor parameter order are acceptable for the ktor-server-di module when the code hasn't been released yet, as confirmed by the development team.

Applied to files:

  • ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt
  • ktor-client/ktor-client-core/api/ktor-client-core.klib.api
📚 Learning: 2025-06-23T12:49:56.883Z
Learnt from: CR
Repo: ktorio/ktor PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-06-23T12:49:56.883Z
Learning: Binary compatibility is enforced using the binary-compatibility-validator plugin.

Applied to files:

  • ktor-client/ktor-client-core/api/ktor-client-core.klib.api
🔇 Additional comments (2)
ktor-client/ktor-client-core/api/ktor-client-core.klib.api (2)

891-907: The review comment is incorrect; FormBuilder is already correctly annotated.

The source code in ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt already implements what the review suggests:

  • Constructor: @PublishedApi internal constructor()
  • build() method: @PublishedApi internal fun build()

The klib.api dump correctly exposes these members because @PublishedApi internal means "internal but part of the public API for inliners." This is the intended design and causes no external compatibility issues since only the inline formData() function uses FormBuilder() directly.

Likely an incorrect or invalid review comment.


1531-1531: Kotlin/Native ABI incompatibility confirmed; verify release status before proceeding.

Web search confirms your concern: changing a function from non-inline to inline is a binary-breaking change for Kotlin/Native klib consumers, as previously compiled call sites expect the original callable symbol, and making the function inline removes/changes that emitted symbol.

However, referencing Ktor team precedent: breaking changes to unreleased code are acceptable per prior PRs (#4916, #4855, #4860). If ktor-client-core hasn't been released yet, this change may be acceptable with a corresponding version bump. Confirm release status; if released, consider preserving a non-inline forwarding symbol to maintain binary compatibility.

@osipxd osipxd requested a review from e5l November 18, 2025 16:17
@osipxd osipxd force-pushed the osipxd/make-formData-inline branch from 2b93efe to c9fcb5c Compare November 18, 2025 16:17
@osipxd osipxd force-pushed the osipxd/make-formData-inline branch from c9fcb5c to b9534b7 Compare November 21, 2025 09:19
@osipxd osipxd enabled auto-merge (squash) November 21, 2025 09:19
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt (1)

89-90: Good use of inline modifier with proper @PublishedApi support.

The inlining is correctly implemented with the necessary @PublishedApi annotations on the internal APIs.

Consider adding a contract to specify that block is called exactly once, consistent with other builder functions in this file (lines 226, 267):

+@OptIn(ExperimentalContracts::class)
 public inline fun formData(block: FormBuilder.() -> Unit): List<PartData> =
-    formData(*FormBuilder().apply(block).build().toTypedArray())
+    formData(*FormBuilder().apply(block).build().toTypedArray()).also {
+        contract {
+            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+        }
+    }

Wait, that won't work with an expression body. Here's the correct approach:

+@OptIn(ExperimentalContracts::class)
-public inline fun formData(block: FormBuilder.() -> Unit): List<PartData> =
-    formData(*FormBuilder().apply(block).build().toTypedArray())
+public inline fun formData(block: FormBuilder.() -> Unit): List<PartData> {
+    contract {
+        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+    }
+    return formData(*FormBuilder().apply(block).build().toTypedArray())
+}
ktor-client/ktor-client-core/api/ktor-client-core.klib.api (1)

891-907: FormBuilder public ctor + build() look coherent with the builder API

Exposing a no-arg constructor and build(): List<FormPart<*>> aligns with the existing append(...) overloads and the top-level formData(vararg FormPart<*>) helper. This makes the builder usable directly while keeping the types consistent. Just be aware this cements FormBuilder as a more fully public construction API that you’ll need to support going forward, but I don’t see any functional or ABI issues here.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9fcb5c and b9534b7.

📒 Files selected for processing (3)
  • ktor-client/ktor-client-core/api/ktor-client-core.api (2 hunks)
  • ktor-client/ktor-client-core/api/ktor-client-core.klib.api (3 hunks)
  • ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ktor-client/ktor-client-core/api/ktor-client-core.api
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-14T18:05:02.321Z
Learnt from: bjhham
Repo: ktorio/ktor PR: 4855
File: ktor-server/ktor-server-plugins/ktor-server-di/api/ktor-server-di.klib.api:334-336
Timestamp: 2025-05-14T18:05:02.321Z
Learning: Breaking changes in constructor parameter order are acceptable for the ktor-server-di module when the code hasn't been released yet, as confirmed by the development team.

Applied to files:

  • ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt
  • ktor-client/ktor-client-core/api/ktor-client-core.klib.api
📚 Learning: 2025-06-23T12:49:56.883Z
Learnt from: CR
Repo: ktorio/ktor PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-06-23T12:49:56.883Z
Learning: Binary compatibility is enforced using the binary-compatibility-validator plugin.

Applied to files:

  • ktor-client/ktor-client-core/api/ktor-client-core.klib.api
🔇 Additional comments (3)
ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt (2)

97-97: Verify the intended visibility of the FormBuilder constructor.

The constructor is marked @PublishedApi internal, which allows inline functions to use it while keeping it out of the public API. However, the AI-generated summary states "Added a public no-arg FormBuilder() constructor," suggesting it should be public.

Please clarify the intended API design:

  • Should FormBuilder be publicly instantiable for direct use outside of the formData builder?
  • Or should it remain internal and only accessible through the formData inline function?

If users need to instantiate FormBuilder directly, change the visibility:

-public class FormBuilder @PublishedApi internal constructor() {
+public class FormBuilder() {

If it should remain internal for inline use only, the current implementation is correct.


215-216: Correct use of @PublishedApi for inline support.

The @PublishedApi annotation is properly applied to allow the inline formData function to access this internal method.

ktor-client/ktor-client-core/api/ktor-client-core.klib.api (1)

1520-1532: Inline formData(FormBuilder.() -> Unit) matches the DSL goal; please just sanity‑check K/N implications

Marking io.ktor.client.request.forms/formData((FormBuilder) -> Unit) as inline satisfies KTOR-9037 and keeps the same parameter/return types as the existing non‑inline variant, so JVM/JS callers remain binary compatible and new code benefits from inlining. For Kotlin/Native this changes klib metadata but still shouldn’t break already-built binaries; the main constraint is that removing inline later would be a breaking change.

Given the earlier question about K/N ABI, I’d recommend double‑checking this behavior against the Kotlin/Native docs or a tiny repro project for your supported compiler versions, but from an API-surface perspective this looks fine and consistent with the rest of the client DSL.

Based on learnings

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants