Low commit activity in last 3 years
Build and parse multipart/form-data request bodies with a clean DSL for adding text fields and file uploads, including automatic MIME type detection, IO streaming, boundary generation, and content type headers.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-multipart

Tests Gem Version Last updated

Multipart/form-data builder and parser with MIME type detection and streaming support

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-multipart"

Or install directly:

gem install philiprehberger-multipart

Usage

require "philiprehberger/multipart"

builder = Philiprehberger::Multipart.build do
  field :name, 'Alice'
  field :email, 'alice@example.com'
end

builder.to_s          # => multipart body string
builder.content_type  # => "multipart/form-data; boundary=..."

File Uploads

builder = Philiprehberger::Multipart.build do
  field :name, 'Alice'
  file :avatar, '/path/to/photo.png'
end

builder.to_s      # => multipart body with file data
builder.boundary  # => the boundary string

Streaming IO Objects

require "stringio"

io = StringIO.new("CSV,data,here")

builder = Philiprehberger::Multipart.build do
  file :upload, io, filename: 'data.csv', content_type: 'text/csv'
end

builder.to_s  # => multipart body streamed from IO

MIME Type Detection

# Auto-detected from filename when content_type is not provided
builder = Philiprehberger::Multipart.build do
  file :doc, '/path/to/report.pdf'       # => application/pdf
  file :img, '/path/to/photo.jpg'        # => image/jpeg
  file :data, '/path/to/config.json'     # => application/json
end

# Direct lookup
Philiprehberger::Multipart::MimeTypes.lookup('photo.jpg')   # => "image/jpeg"
Philiprehberger::Multipart::MimeTypes.lookup('unknown.xyz')  # => "application/octet-stream"

Parsing Multipart Bodies

body = "--boundary\r\n" \
       "Content-Disposition: form-data; name=\"field\"\r\n" \
       "\r\n" \
       "value\r\n" \
       "--boundary--\r\n"

parts = Philiprehberger::Multipart.parse(body, content_type: 'multipart/form-data; boundary=boundary')

parts.first.name          # => :field
parts.first.body          # => "value"
parts.first.file?         # => false

Streaming Output

builder = Philiprehberger::Multipart.build do
  field :name, 'Alice'
  file :avatar, '/path/to/photo.png'
end

# Stream directly to a file or socket without buffering the full body
File.open('body.dat', 'wb') { |f| builder.write_to(f) }

# Content-Length for HTTP headers
builder.content_length  # => 1234
builder.headers         # => { "Content-Type" => "multipart/form-data; boundary=...", "Content-Length" => "1234" }

Listing Field Names

builder = Philiprehberger::Multipart.build do |b|
  b.field("name", "Alice")
  b.file("avatar", "avatar.png")
end
builder.field_names # => ["name", "avatar"]

Post-Construction Part Lookup

builder = Philiprehberger::Multipart.build do
  field :name, 'Alice'
  file :avatar, '/path/to/photo.png'
end

# Look up a part by name (String or Symbol both work)
builder.part(:avatar)   # => #<Part name=:avatar ...>
builder.part('avatar')  # => same Part

# Tweak attributes after the fact — changes flow through to #to_s
builder.part('avatar').content_type = 'image/webp'

# Returns nil when no part matches
builder.part(:missing)  # => nil

Inspecting the Builder

builder = Philiprehberger::Multipart.build do
  field :name, 'Alice'
  file :avatar, '/path/to/photo.png'
end

builder.size  # => same as builder.content_length
builder.to_h
# => {
#      boundary: "----PhiliprehbergerMultipart...",
#      parts: [
#        { name: "name",   filename: nil,            content_type: nil,         size: 5 },
#        { name: "avatar", filename: "photo.png",    content_type: "image/png", size: 4096 }
#      ]
#    }

Custom Boundary

builder = Philiprehberger::Multipart.build(boundary: 'my-boundary') do
  field :key, 'value'
end

builder.content_type  # => "multipart/form-data; boundary=my-boundary"

API

Method Description
Multipart.build(boundary: nil, &block) Build a multipart body using the DSL
Multipart.parse(body, content_type:) Parse an incoming multipart/form-data body
MimeTypes.lookup(filename) Look up MIME type from a filename extension
Builder#field(name, value) Add a text field
Builder#file(name, path_or_io, filename:, content_type:) Add a file from path or IO object
Builder#part(name) Look up the first part with a matching name (Symbol or String), or nil
Builder#field_names Array of part names (as strings) in insertion order
Builder#merge(other) Append parts from another Builder without re-encoding
Builder#to_s Render the multipart body as a string
Builder#content_type Content-Type header value with boundary
Builder#boundary The multipart boundary string
Builder#write_to(io) Stream the multipart body to an IO object
Builder#content_length Byte size of the body for Content-Length headers
Builder#size Alias for content_length
Builder#to_h Structured { boundary:, parts: [...] } summary
Builder#headers Hash with Content-Type and Content-Length headers
Part#name The field name
Part#value The part value / body content
Part#body Alias for value
Part#filename The original filename (nil for text fields)
Part#content_type The MIME content type (nil for text fields)
Part#file? Whether this part is a file upload
Part#text? Whether this part is a plain text field (inverse of file?)
Part#size Byte size of the part's value

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT