Skip to content

fix(form): rename blank add_field name to _unnamed_<n> instead of emitting empty name#60

Merged
nao1215 merged 1 commit into
mainfrom
fix/issue-58-add-field-empty-name
May 31, 2026
Merged

fix(form): rename blank add_field name to _unnamed_<n> instead of emitting empty name#60
nao1215 merged 1 commit into
mainfrom
fix/issue-58-add-field-empty-name

Conversation

@nao1215

@nao1215 nao1215 commented May 31, 2026

Copy link
Copy Markdown
Owner

Summary

The lenient multipartkit/form.add_field silently produced a part with Content-Disposition: form-data; name="" when given an empty name, or a name that became empty after CR/LF/NUL stripping. RFC 7578 §4.2 requires the name parameter to be the field name, and an empty name produces a wire image whose receiver interpretation is implementation-defined (skip the field, overwrite siblings keyed on "", or reject the whole body). Only add_field_strict rejected this; the default path was a silent data-loss / non-compliant-wire vector. This closes the two regression reports #57 (CR/LF stripped to empty) and #58 (empty name accepted), which share the same root cause.

Changes

This implements the issues' "option 2: normalise loudly". The lenient add_field now renames a blank name (empty, or empty/whitespace-only after stripping) to a generated "_unnamed_<n>" placeholder, where <n> is the part's zero-based position in the form. The observable name is therefore never Some(""), the part stays RFC 7578-addressable, and the add_field signature stays -> Form (no breaking API change). A name that is merely padded (e.g. " a ") or only partially stripped (e.g. "ab\ncd" -> "abcd") is kept as-is — only an entirely blank name is rewritten.

add_field_strict is unchanged: callers that want bad names surfaced as Error(EmptyFieldName(_)) / Error(NameContainsControlBytes(_)) rather than renamed should keep using it.

Added test/regression_add_field_empty_name_test.gleam covering the empty / CR-LF-only / NUL-only / whitespace-only cases, the partial-strip and padded cases that must be kept verbatim, the normal-name baseline, and placeholder index tracking across interleaved blank and non-blank fields.

Design Decisions

Option 2 was chosen over option 1 (reject in add_field) and option 3 (reject at encode_form) because it is the only option that both satisfies the issues' Definition-of-Done (name never round-trips to Some("")) and keeps the existing backward-compatibility pin add_field_non_strict_empty_name_still_accepted_test green — that test asserts the non-strict path still produces exactly one part, so dropping the part (option 1) or returning a Result from encode_form (option 3) would have been a larger, breaking change. Renaming preserves the caller's value under a visible, detectable synthetic name rather than discarding it.

The whitespace-only case is normalised too, matching the "empty" notion that add_field_strict already rejects via string.trim.

Limitations

The _unnamed_<n> placeholder is position-based, not collision-proof: a caller who also supplies a literal "_unnamed_<k>" as a real field name can end up with two parts sharing that name. This is documented on add_field and is strictly better than the previous name="" behaviour.

add_file's name channel (and the filename channel) are intentionally left unchanged — both issues explicitly defer add_file to a separate ticket.

Closes #57
Closes #58

Summary by CodeRabbit

  • Bug Fixes
    • Fixed add_field to prevent empty field names from being emitted. Names that become empty after sanitization are now automatically replaced with generated placeholders (_unnamed_<n>) based on field position, ensuring RFC 7578 compliance.

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes empty or whitespace-only field names in the non-strict add_field builder by replacing them with generated "_unnamed_<n>" positional placeholders after sanitization. The implementation includes a new internal helper, updated module documentation, comprehensive regression tests, and a changelog entry.

Changes

Empty field name placeholder handling

Layer / File(s) Summary
Core empty-name placeholder implementation
src/multipartkit/form.gleam
Imports gleam/int, documents the placeholder generation behavior, adds an internal ensure_named(form, safe_name) helper that returns the name when non-blank after trimming, otherwise generates "_unnamed_<n>" using the part count, and modifies add_field to apply this guarantee after CR/LF/NUL sanitization.
Regression test suite
test/regression_add_field_empty_name_test.gleam
New test module with eight tests verifying that empty, CR/LF, and whitespace-only names round-trip to "_unnamed_<n>" placeholders, partially-stripped names are kept as non-empty normalized values, padded names are preserved verbatim, and placeholder indices remain position-based without collision across multiple fields.
Changelog release note
CHANGELOG.md
Unreleased fixed entry documenting that add_field no longer emits name="" on the wire and directing users to add_field_strict for typed error handling.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • nao1215/multipartkit#57: This PR directly fixes the reported bug by implementing the "_unnamed_<n>" placeholder replacement for empty/CRLF-stripped names.
  • nao1215/multipartkit#58: This PR implements the suggested placeholder renaming strategy to avoid emitting empty name="" values on the wire.

Possibly related PRs

  • nao1215/multipartkit#56: Introduces add_field_strict to reject empty field names with typed errors, complementing this PR's lenient placeholder approach for the same field-name validation concern.
  • nao1215/multipartkit#38: Refactors Content-Disposition name sanitization helpers in the shared internal/disposition logic that this PR's add_field modification relies upon for CR/LF/NUL stripping.

Poem

A rabbit hops through empty names,
Renaming them with indexed claims,
No more blank wires, void, or shame—
Placeholders mark the unnamed game! 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: the fix to the add_field function that renames blank field names to a generated placeholder format instead of emitting empty names, which is exactly what the changeset implements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-58-add-field-empty-name

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/multipartkit/form.gleam (1)

181-182: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update add_field_strict documentation to reflect the new add_field behavior.

The docstring states that add_field("name\n", _) produces a part with name="", but after this PR's changes, it now produces a part with name="_unnamed_0" (or the appropriate position-based placeholder). This documentation should be updated to accurately describe the current behavior of the non-strict variant, so users understand the difference between strict and non-strict paths.

For example:

/// The non-strict `add_field` silently strips control bytes and renames
/// empty or stripped-to-empty names to a generated placeholder (so
/// `add_field("name\n", _)` produces a part with `name="_unnamed_0"`).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/multipartkit/form.gleam` around lines 181 - 182, Update the
add_field_strict docstring to accurately describe the new behavior of the
non-strict variant: explain that add_field (non-strict) strips control bytes
and, when a name becomes empty after stripping, replaces it with a generated
placeholder (e.g. "_unnamed_0" or position-based placeholder) rather than
producing an empty name; reference add_field and add_field_strict so maintainers
can locate the docs to change the example text accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/multipartkit/form.gleam`:
- Around line 181-182: Update the add_field_strict docstring to accurately
describe the new behavior of the non-strict variant: explain that add_field
(non-strict) strips control bytes and, when a name becomes empty after
stripping, replaces it with a generated placeholder (e.g. "_unnamed_0" or
position-based placeholder) rather than producing an empty name; reference
add_field and add_field_strict so maintainers can locate the docs to change the
example text accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 358a9b46-1b48-4f3c-88e6-69f7a677c7e3

📥 Commits

Reviewing files that changed from the base of the PR and between 37477cd and a57e69d.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/multipartkit/form.gleam
  • test/regression_add_field_empty_name_test.gleam

@nao1215 nao1215 merged commit b18c0ed into main May 31, 2026
5 checks passed
@nao1215 nao1215 deleted the fix/issue-58-add-field-empty-name branch May 31, 2026 08:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant