HTTP.FormData (http_core v0.21.19)

Copy Markdown View Source

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.Stream values 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.

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

form_part()

@type form_part() ::
  {:field, String.t(), String.t()}
  | {:file, String.t(), String.t(), String.t(), File.Stream.t()}
  | {:file, String.t(), String.t(), String.t(), String.t()}

stream_envelope()

@type stream_envelope() :: %{
  body: Enumerable.t(),
  content_length: non_neg_integer(),
  content_type: String.t(),
  boundary: String.t()
}

t()

@type t() :: %HTTP.FormData{boundary: String.t() | nil, parts: [form_part()]}

Functions

append_field(form, name, value)

@spec append_field(t(), String.t(), String.t()) :: t()

Adds a form field.

Examples

iex> HTTP.FormData.new() |> HTTP.FormData.append_field("name", "value")
%HTTP.FormData{parts: [{:field, "name", "value"}], boundary: nil}

append_file(form, name, filename, content, content_type \\ "application/octet-stream")

@spec append_file(
  t(),
  String.t(),
  String.t(),
  File.Stream.t() | String.t(),
  String.t()
) :: t()

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}

generate_boundary()

@spec generate_boundary() :: String.t()

Generates a random boundary for multipart/form-data.

get_content_type(form_data)

@spec get_content_type(t()) :: String.t()

Gets the appropriate Content-Type header for the form data.

new()

@spec new() :: t()

Creates a new empty FormData struct.

Examples

iex> HTTP.FormData.new()
%HTTP.FormData{parts: [], boundary: nil}

to_body(form)

@spec to_body(t()) :: {:url_encoded, String.t()} | {:multipart, iodata(), String.t()}

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.

to_stream(form, options \\ [])

@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.