Skip to content

Client. Always quote name and filename in multipart Content-Disposition#5573

Merged
bjhham merged 2 commits intoktorio:mainfrom
fru1tworld:fix/5157-multipart-quote
May 5, 2026
Merged

Client. Always quote name and filename in multipart Content-Disposition#5573
bjhham merged 2 commits intoktorio:mainfrom
fru1tworld:fix/5157-multipart-quote

Conversation

@fru1tworld
Copy link
Copy Markdown
Contributor

Subsystem
Client Core

Motivation
Fixes #5157

Ktor emits unquoted name=/filename= in multipart Content-Disposition. Strict HTML-spec parsers (e.g. Elysia on Bun) silently drop unquoted parameters, while browsers, curl, OkHttp, and other major clients always quote.

Solution

  • Always quote name and filename via a small quoteForMultipart() helper that skips re-quoting if the value is already quoted.
  • Promote String.isQuoted() to @InternalAPI public so the multipart writer in ktor-client-core can reuse it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9dd1e019-fc17-4acd-ae5c-e1e38bac0d0d

📥 Commits

Reviewing files that changed from the base of the PR and between 11a6ce5 and c9130b1.

📒 Files selected for processing (2)
  • ktor-client/ktor-client-core/common/test/MultiPartFormDataContentTest.kt
  • ktor-client/ktor-client-plugins/ktor-client-logging/jvm/test/io/ktor/client/plugins/logging/OkHttpFormatTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • ktor-client/ktor-client-core/common/test/MultiPartFormDataContentTest.kt

📝 Walkthrough

Walkthrough

This PR changes multipart form-data serialization so Content-Disposition parameters use quoted-string formatting. name and filename are now emitted via a new quoteForMultipart() helper (opted into @InternalAPI) instead of escapeIfNeeded(). isQuoted() is promoted to @InternalAPI public. Tests updated accordingly.

Changes

Multipart Content-Disposition Quoting

Layer / File(s) Summary
Public Utilities
ktor-http/common/src/io/ktor/http/HeaderValueWithParameters.kt, ktor-http/api/ktor-http.api, ktor-http/api/ktor-http.klib.api
String.isQuoted() promoted to @InternalAPI public and added to API surface; determines whether a string is already quoted.
Form DSL Helpers
ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt
Added internal fun String.quoteForMultipart() (opts into InternalAPI) that returns the original string if isQuoted() else returns quote().
Core Form DSL Implementation
ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt
formData() and FormBuilder.append() now use quoteForMultipart() for name and filename parameters when building Content-Disposition.
Web Platform Override
ktor-client/ktor-client-core/web/src/io/ktor/client/request/forms/FormDsl.web.kt
FormBuilder.appendBlob() changed to use filename.quoteForMultipart() instead of escapeIfNeeded().
Tests / Assertions
ktor-client/ktor-client-core/common/test/FormDslTest.kt, .../MultiPartFormDataContentTest.kt, .../js/.../FormDslJsTest.kt, .../wasmJs/.../FormDslWasmJsTest.kt, ktor-client-tests/.../ContentTest.kt, ktor-client-plugins/.../OkHttpFormatTest.kt, ktor-client-tests/.../LoggingMockedTests.kt
Updated multipart Content-Disposition expectations to require quoted name/filename (e.g., name="...", filename="..."); added tests asserting name/filename are always quoted and pre-quoted values are not double-quoted.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ktorio/ktor#5390: Overlaps changes to appendBlob/form DSL multipart handling and Content-Disposition formatting.

Suggested labels

👍 ship!

Suggested reviewers

  • e5l
  • osipxd
  • bjhham
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely describes the main change: always quoting name and filename in multipart Content-Disposition headers.
Description check ✅ Passed Description follows the template with all required sections: Subsystem (Client Core), Motivation (references #5157 with clear problem statement), and Solution (details the quoteForMultipart helper and isQuoted promotion).
Linked Issues check ✅ Passed The PR fully addresses issue #5157 by implementing quoting for name and filename parameters in multipart Content-Disposition headers, resolving the incompatibility with strict parsers.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the multipart quoting objective: formDsl updates, helper function addition, API surface changes, and corresponding test updates across all platforms.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ktor-http/common/src/io/ktor/http/HeaderValueWithParameters.kt (1)

126-149: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

isQuoted() accepts values with an escaped closing quote.

Line 132 treats the last " as a valid closing delimiter without verifying it is not escaped, so malformed input like "abc\" can be reported as quoted.

💡 Suggested fix
 public fun String.isQuoted(): Boolean {
     if (length < 2) {
         return false
     }
     if (first() != '"' || last() != '"') {
         return false
     }
+    var trailingSlashes = 0
+    var trailingSlashIndex = lastIndex - 1
+    while (trailingSlashIndex >= 0 && this[trailingSlashIndex] == '\\') {
+        trailingSlashes++
+        trailingSlashIndex--
+    }
+    if (trailingSlashes % 2 != 0) {
+        return false
+    }
     var startIndex = 1
     do {
         val index = indexOf('"', startIndex)
         if (index == lastIndex) {
             break
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ktor-http/common/src/io/ktor/http/HeaderValueWithParameters.kt` around lines
126 - 149, The isQuoted() routine incorrectly treats the final double-quote as a
valid closing delimiter without verifying it's not escaped; update the logic in
isQuoted() to check that the quote found at lastIndex is not escaped by counting
preceding backslashes (like you do for other quotes) and only accept it as a
closing quote when the number of preceding '\' characters is even (i.e. the
quote is not escaped); concretely, in the loop that finds index = indexOf('"',
startIndex) handle the case index == lastIndex by computing slashesCount the
same way you compute it for other quotes and return false if that quote is
escaped, otherwise accept and break/return true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ktor-client/ktor-client-core/common/test/MultiPartFormDataContentTest.kt`:
- Around line 251-269: The test title is misleading:
MultiPartFormDataContentTest.`filename parameter is always quoted in
Content-Disposition` asserts an unquoted filename because the header was
injected raw and bypasses quoteForMultipart via formData() ->
appendAll(headers); rename the test to reflect actual behavior (e.g., "filename
parameter is not quoted when provided in raw Content-Disposition header") or
alternatively change the setup to let the library produce the
Content-Disposition (remove the raw header and rely on append(key=...,
filename=...)) so the assertion matches the title; refer to
MultiPartFormDataContent, formData, appendAll(headers), and quoteForMultipart
when making the change.

---

Outside diff comments:
In `@ktor-http/common/src/io/ktor/http/HeaderValueWithParameters.kt`:
- Around line 126-149: The isQuoted() routine incorrectly treats the final
double-quote as a valid closing delimiter without verifying it's not escaped;
update the logic in isQuoted() to check that the quote found at lastIndex is not
escaped by counting preceding backslashes (like you do for other quotes) and
only accept it as a closing quote when the number of preceding '\' characters is
even (i.e. the quote is not escaped); concretely, in the loop that finds index =
indexOf('"', startIndex) handle the case index == lastIndex by computing
slashesCount the same way you compute it for other quotes and return false if
that quote is escaped, otherwise accept and break/return true.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ea736cb-f2e1-4e7f-b553-cc3e5006caef

📥 Commits

Reviewing files that changed from the base of the PR and between 295e688 and 11a6ce5.

📒 Files selected for processing (11)
  • ktor-client/ktor-client-core/common/src/io/ktor/client/request/forms/formDsl.kt
  • ktor-client/ktor-client-core/common/test/FormDslTest.kt
  • ktor-client/ktor-client-core/common/test/MultiPartFormDataContentTest.kt
  • ktor-client/ktor-client-core/js/test/io/ktor/client/request/forms/FormDslJsTest.kt
  • ktor-client/ktor-client-core/wasmJs/test/io/ktor/client/request/forms/FormDslWasmJsTest.kt
  • ktor-client/ktor-client-core/web/src/io/ktor/client/request/forms/FormDsl.web.kt
  • ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/ContentTest.kt
  • ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingMockedTests.kt
  • ktor-http/api/ktor-http.api
  • ktor-http/api/ktor-http.klib.api
  • ktor-http/common/src/io/ktor/http/HeaderValueWithParameters.kt

Copy link
Copy Markdown
Contributor

@bjhham bjhham left a comment

Choose a reason for hiding this comment

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

Please address the rabbit's comment regarding the test name contradiction and fix the assertion in io.ktor.client.plugins.logging.OkHttpFormatTest

- Rewrite filename quoting test to use FormBuilder.append(key, filename, ...) so the library's quoteForMultipart path is actually exercised, matching the test name.
- Update OkHttpFormatTest.multipartBinaryBody assertions and Content-Length to reflect the new quoted name parameters.
@fru1tworld
Copy link
Copy Markdown
Contributor Author

Addressed in c9130b1 — fixed the test name/behavior mismatch and updated OkHttpFormatTest assertions.

Copy link
Copy Markdown
Contributor

@bjhham bjhham left a comment

Choose a reason for hiding this comment

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

Thank you for the fix! I've uncovered an old issue in YouTrack that maps directly to this problem and was closed incorrectly. KTOR-455

@bjhham bjhham merged commit 0219a77 into ktorio:main May 5, 2026
17 of 19 checks passed
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.

Client: Multipart upload doesn't work with some servers

2 participants