feat: support multipart/form-data request bodies with file attachments#56
Open
danielbowne wants to merge 4 commits into
Open
feat: support multipart/form-data request bodies with file attachments#56danielbowne wants to merge 4 commits into
danielbowne wants to merge 4 commits into
Conversation
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
devopsmatt
approved these changes
Apr 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
multipartbody type to test definitions, withfieldsfor plain key/value form parts andfilesfor local-path file attachments. Closes #52 and #53.Changes
internal/engine/test.go: addmultipartBodystruct andbody.Multipartfieldinternal/engine/multipart.go: new helper that builds the multipart payload, derives per-fileContent-Typefrom the extension, and caps each file at 50 MiB to bound in-memory bufferinginternal/engine/engine.go: route the multipart branch through the existing body-building flow, expand the body mutual-exclusion guard to covermultipart, and surface validation errors through the post-NewRequestreporter so they are counted as failed tests\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 viaContent-DispositionquoteEscaperpattern thatmime/multipart.CreateFormFileuses, keeping non-ASCII filenames as raw UTF-8 for lenient-server interoptests/: 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 guardREADME.md: schema reference and a usage example for the new option.gitignore: exclude the locally builtemberfallbinary so it does not get committed alongside source changesTest plan
go build ./...cleango vet ./...cleanbats tests/cli.bats(30 pre-existing + 6 new)postman-echo.com/postmultipart file ... no such file or directoryerror and is counted as a failed test\r\nis rejected before the request is built\r\nin the basename is rejected beforeos.Openmultipartandjsonon the same test is rejected withbody may define only one of json, text, or multipart