HTTP form data and multipart/form-data encoding for file uploads.
This module provides a convenient API for building form submissions with support for both URL-encoded forms and multipart file uploads. It automatically chooses the appropriate encoding based on the presence of file fields.
Encoding Selection
- URL-encoded (
application/x-www-form-urlencoded): Used when form contains only text fields - Multipart (
multipart/form-data): Used when form contains file fields
Features
- File stream inputs: Accepts
File.Streamvalues when building multipart bodies - Automatic encoding: Selects appropriate encoding based on content
- Boundary generation: Automatically generates unique multipart boundaries
- Mixed content: Support for both text fields and files in the same form
Basic Usage
# Simple form with text fields
form = HTTP.FormData.new()
|> HTTP.FormData.append_field("username", "john_doe")
|> HTTP.FormData.append_field("email", "john@example.com")
HTTP.fetch("https://api.example.com/signup", method: "POST", body: form)File Upload
# Single file upload
file_stream = File.stream!("document.pdf")
form = HTTP.FormData.new()
|> HTTP.FormData.append_field("title", "My Document")
|> HTTP.FormData.append_file("document", "document.pdf", file_stream, "application/pdf")
HTTP.fetch("https://api.example.com/upload", method: "POST", body: form)Multiple Files
# Upload multiple files
form = HTTP.FormData.new()
|> HTTP.FormData.append_field("description", "Photos from vacation")
|> HTTP.FormData.append_file("photo1", "beach.jpg", File.stream!("beach.jpg"), "image/jpeg")
|> HTTP.FormData.append_file("photo2", "sunset.jpg", File.stream!("sunset.jpg"), "image/jpeg")
HTTP.fetch("https://api.example.com/gallery", method: "POST", body: form)Content Types
When uploading files, you can specify the MIME type. If not provided, it defaults
to "application/octet-stream":
# With explicit content type
form |> HTTP.FormData.append_file("image", "photo.jpg", stream, "image/jpeg")
# With default content type
form |> HTTP.FormData.append_file("data", "data.bin", stream)
Summary
Functions
Adds a form field.
Adds a file field for upload.
Generates a random boundary for multipart/form-data.
Gets the appropriate Content-Type header for the form data.
Creates a new empty FormData struct.
Converts FormData to HTTP body content with appropriate encoding.
Builds a lazy multipart envelope with an exact byte length.
Types
@type stream_envelope() :: %{ body: Enumerable.t(), content_length: non_neg_integer(), content_type: String.t(), boundary: String.t() }
Functions
Adds a form field.
Examples
iex> HTTP.FormData.new() |> HTTP.FormData.append_field("name", "value")
%HTTP.FormData{parts: [{:field, "name", "value"}], boundary: nil}
Adds a file field for upload.
Examples
iex> file_stream = File.stream!("test.txt")
iex> HTTP.FormData.new() |> HTTP.FormData.append_file("upload", "test.txt", file_stream)
%HTTP.FormData{parts: [{:file, "upload", "test.txt", "text/plain", %File.Stream{}}], boundary: nil}
@spec generate_boundary() :: String.t()
Generates a random boundary for multipart/form-data.
Gets the appropriate Content-Type header for the form data.
@spec new() :: t()
Creates a new empty FormData struct.
Examples
iex> HTTP.FormData.new()
%HTTP.FormData{parts: [], boundary: nil}
Converts FormData to HTTP body content with appropriate encoding.
Returns {:url_encoded, body} for regular forms or {:multipart, body, boundary} for multipart. The multipart body is returned as iodata for memory efficiency with large file uploads.
@spec to_stream(t(), keyword()) :: {:ok, stream_envelope()} | {:error, term()}
Builds a lazy multipart envelope with an exact byte length.
Returns {:ok, %{body: enumerable, content_length: bytes, content_type: type, boundary: boundary}}. Construction stats regular file paths but does not open
or read them. Enumeration opens one file at a time and emits binary chunks of
at most :chunk_size bytes (default 32,768; maximum 65,536). Halting enumeration
closes the current file. The existing to_body/1 buffered API is unchanged.
For Fetch uploads, adapt the enumerable through HTTP.Stream.from_enumerable/1
and provide the matching length and content type:
{:ok, multipart} = HTTP.FormData.to_stream(form)
{:ok, upload} = HTTP.Stream.from_enumerable(multipart.body)
HTTP.fetch(url,
method: :post,
body: upload,
duplex: :half,
content_type: multipart.content_type,
headers: [{"Content-Length", Integer.to_string(multipart.content_length)}]
)File inputs must be local regular File.Stream paths with byte-preserving
modes. Default line-mode streams are reopened in binary chunks. Encoded,
compressed, remote-node and write-mode streams are rejected because their
output length cannot be obtained from file metadata alone. Field values and
in-memory file content must be binaries.
Keep files unchanged until upload completion. Fetch's fixed-length upload checks reject underflow or overflow if a file's size changes. Creating the envelope starts no producer; Fetch's upload stream owns enumeration and its existing cancellation, failed-connection and owner-exit cleanup applies. The serializer does not retry uploads.