Skip to content

feat: support multipart/form-data request bodies with file attachments#56

Open
danielbowne wants to merge 4 commits into
mainfrom
feat/multipart-and-file-bodies
Open

feat: support multipart/form-data request bodies with file attachments#56
danielbowne wants to merge 4 commits into
mainfrom
feat/multipart-and-file-bodies

Conversation

@danielbowne

Copy link
Copy Markdown
Collaborator

Summary

Adds a multipart body type to test definitions, with fields for plain key/value form parts and files for local-path file attachments. Closes #52 and #53.

Changes

  • internal/engine/test.go: add multipartBody struct and body.Multipart field
  • internal/engine/multipart.go: new helper that builds the multipart payload, derives per-file Content-Type from the extension, and caps each file at 50 MiB to bound in-memory buffering
  • internal/engine/engine.go: route the multipart branch through the existing body-building flow, expand the body mutual-exclusion guard to cover multipart, and surface validation errors through the post-NewRequest reporter so they are counted as failed tests
  • Reject any control character (\r, \n, \x00, and the rest of the C0/C1 range) in field names, file form names, file paths, and basenames so a YAML author cannot inject extra MIME headers via Content-Disposition
  • Quote field and filename parameters with the same quoteEscaper pattern that mime/multipart.CreateFormFile uses, keeping non-ASCII filenames as raw UTF-8 for lenient-server interop
  • tests/: fixtures and bats coverage for fields-only, fields + file echo, missing file error, CRLF rejection on field names, CRLF rejection on filenames, and the body mutual-exclusion guard
  • README.md: schema reference and a usage example for the new option
  • .gitignore: exclude the locally built emberfall binary so it does not get committed alongside source changes

Test plan

  • go build ./... clean
  • go vet ./... clean
  • All 36 bats cases pass: bats tests/cli.bats (30 pre-existing + 6 new)
  • Fields-only round-trip echoes through postman-echo.com/post
  • File attachment round-trip asserts the deterministic base64 echo of the upload
  • Missing file path produces a clear multipart file ... no such file or directory error and is counted as a failed test
  • Field name with embedded \r\n is rejected before the request is built
  • File path with embedded \r\n in the basename is rejected before os.Open
  • Defining both multipart and json on the same test is rejected with body may define only one of json, text, or multipart

Add a multipart body type alongside json and text, with two sub-keys:
fields for plain key/value form parts and files for local-path file
attachments. File parts get a Content-Type derived from the extension
(falling back to application/octet-stream), and missing file paths
produce a clear error.

Closes #52, closes #53.
- reject part names containing CR, LF, or NUL to block MIME header
  injection from YAML-controlled field and file form names
- cap each multipart file at 50 MiB via io.LimitedReader so a config
  pointing at /dev/urandom or a sparse file cannot exhaust memory
- assert echoed file content in the file-attachment fixture so the
  test catches a regression where the file part is silently dropped
- add a fixture and bats case for the CRLF-name rejection path
- document body.multipart in the schema reference and add a usage
  example to the README
- reject any unicode control character in field and file form names,
  not only CR, LF, and NUL, so byte sequences such as 0x01-0x1f cannot
  produce malformed Content-Disposition parameters that confuse
  intermediate proxies
- replace fmt.Sprintf with %q quoting in the file part header with the
  same quoteEscaper pattern that mime/multipart.CreateFormFile uses,
  which keeps non-ASCII filenames as raw UTF-8 instead of Go escape
  sequences and improves interop with lenient servers
- repoint the CRLF-rejection fixture URL at 127.0.0.1:0 so the test
  remains offline and cannot be affected by external availability
- clarify the file size cap doc comment to state that the limit is
  per file, not aggregate, so operators understand what the guard
  actually protects against
- validate the file path and its basename for control characters before
  os.Open so that a YAML-supplied path cannot inject CRLF or other
  control bytes into the filename parameter of Content-Disposition;
  this closes the symmetric gap to the field-name validation
- drop the early continue in the body mutual-exclusion guard so the
  failure surfaces through the existing post-NewRequest reporter and
  is counted as a failed test rather than silently skipped
- add bats coverage for the filename CRLF rejection and for the
  json+multipart combined-body rejection
- repoint the missing-file fixture URL at 127.0.0.1:0 so the test no
  longer references postman-echo for a path it never reaches
- note in writeMultipart that part ordering follows Go map iteration
  and is not guaranteed
@danielbowne danielbowne requested a review from devopsmatt April 30, 2026 13:47

@sbolel sbolel left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nice

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.

Support multipart/form-data request bodies

3 participants