Message Body
This guide explains how to work with HTTP request and response message bodies using Protocol::HTTP::Body classes.
Overview
HTTP message bodies represent the actual (often stateful) data content of requests and responses. Protocol::HTTP provides a rich set of body classes for different use cases, from simple string content to streaming data and file serving.
All body classes inherit from class Protocol::HTTP::Body::Readable, which provides a consistent interface for reading data in chunks. Bodies can be:
- Buffered: All content stored in memory.
- Streaming: Content generated or read on-demand.
- File-based: Content read directly from files.
- Transforming: Content modified as it flows through e.g. compression, encryption.
Core Body Interface
Every body implements the Readable interface:
# Read the next chunk of data:
chunk = body.read
# => "Hello" or nil when finished
# Check if body has data available without blocking:
body.ready? # => true/false
# Check if body is empty:
body.empty? # => true/false
# Close the body and release resources:
body.close
# Iterate through all chunks:
body.each do |chunk|
puts chunk
end
# Read entire body into a string:
content = body.join
Buffered Bodies
Use class Protocol::HTTP::Body::Buffered for content that's fully loaded in memory:
# Create from string:
body = Protocol::HTTP::Body::Buffered.new(["Hello", " ", "World"])
# Create from array of strings:
chunks = ["First chunk", "Second chunk", "Third chunk"]
body = Protocol::HTTP::Body::Buffered.new(chunks)
# Wrap various types automatically:
body = Protocol::HTTP::Body::Buffered.wrap("Simple string")
body = Protocol::HTTP::Body::Buffered.wrap(["Array", "of", "chunks"])
# Access properties:
body.length # => 13 (total size in bytes)
body.empty? # => false
body.ready? # => true (always ready)
# Reading:
first_chunk = body.read # => "Hello"
second_chunk = body.read # => " "
third_chunk = body.read # => "World"
fourth_chunk = body.read # => nil (finished)
# Rewind to beginning:
body.rewind
body.read # => "Hello" (back to start)
Buffered Body Features
# Check if rewindable:
body.rewindable? # => true for buffered bodies
# Get all content as single string:
content = body.join # => "Hello World"
# Convert to array of chunks:
chunks = body.to_a # => ["Hello", " ", "World"]
# Write additional chunks:
body.write("!")
body.join # => "Hello World!"
# Clear all content:
body.clear
body.empty? # => true
File Bodies
Use class Protocol::HTTP::Body::File for serving files efficiently:
require "protocol/http/body/file"
# Open a file:
body = Protocol::HTTP::Body::File.open("/path/to/file.txt")
# Create from existing File object:
file = File.open("/path/to/image.jpg", "rb")
body = Protocol::HTTP::Body::File.new(file)
# Serve partial content (ranges):
range = 100...200 # bytes 100-199
body = Protocol::HTTP::Body::File.new(file, range)
# Properties:
body.length # => file size or range size
body.empty? # => false (unless zero-length file)
body.ready? # => false (may block when reading)
# File bodies read in chunks automatically:
body.each do |chunk|
# Process each chunk (typically 64KB)
puts "Read #{chunk.bytesize} bytes"
end
File Body Range Requests
# Serve specific byte ranges (useful for HTTP range requests):
file = File.open("large_video.mp4", "rb")
# First 1MB:
partial_body = Protocol::HTTP::Body::File.new(file, 0...1_048_576)
# Custom block size for reading:
body = Protocol::HTTP::Body::File.new(file, block_size: 8192) # 8KB chunks
Writable Bodies
Use class Protocol::HTTP::Body::Writable for dynamic content generation:
require "protocol/http/body/writable"
# Create a writable body:
body = Protocol::HTTP::Body::Writable.new
# Write data in another thread/fiber:
Thread.new do
body.write("First chunk\n")
sleep 0.1
body.write("Second chunk\n")
body.write("Final chunk\n")
body.close_write # Signal no more data
end
# Read from main thread:
body.each do |chunk|
puts "Received: #{chunk}"
end
# Output:
# Received: First chunk
# Received: Second chunk
# Received: Final chunk
Writable Body with Backpressure
# Use SizedQueue to limit buffering:
queue = Thread::SizedQueue.new(10) # Buffer up to 10 chunks
body = Protocol::HTTP::Body::Writable.new(queue: queue)
# Writing will block if queue is full:
body.write("chunk 1")
# ... write up to 10 chunks before blocking
Streaming Bodies
Use module Protocol::HTTP::Body::Streamable for computed content:
require "protocol/http/body/streamable"
# Generate content dynamically:
body = Protocol::HTTP::Body::Streamable.new do |output|
10.times do |i|
output.write("Line #{i}\n")
# Could include delays, computation, database queries, etc.
end
end
# Content is generated as it's read:
body.each do |chunk|
puts "Got: #{chunk}"
end
Stream Bodies (IO Wrapper)
Use class Protocol::HTTP::Body::Stream to wrap IO-like objects:
require "protocol/http/body/stream"
# Wrap an IO object:
io = StringIO.new("Hello\nWorld\nFrom\nStream")
body = Protocol::HTTP::Body::Stream.new(io)
# Read line by line:
line1 = body.gets # => "Hello\n"
line2 = body.gets # => "World\n"
# Read specific amounts:
data = body.read(5) # => "From\n"
# Read remaining data:
rest = body.read # => "Stream"
Body Transformations
Compression Bodies
require "protocol/http/body/deflate"
require "protocol/http/body/inflate"
# Compress a body:
original = Protocol::HTTP::Body::Buffered.new(["Hello World"])
compressed = Protocol::HTTP::Body::Deflate.new(original)
# Decompress a body:
decompressed = Protocol::HTTP::Body::Inflate.new(compressed)
content = decompressed.join # => "Hello World"
Wrapper Bodies
Create custom body transformations:
require "protocol/http/body/wrapper"
class UppercaseBody < Protocol::HTTP::Body::Wrapper
def read
if chunk = super
chunk.upcase
end
end
end
# Use the wrapper:
original = Protocol::HTTP::Body::Buffered.wrap("hello world")
uppercase = UppercaseBody.new(original)
content = uppercase.join # => "HELLO WORLD"
Life-cycle
Bodies model application-facing streams. Their close operations describe what the application will do next; protocol implementations are responsible for mapping those operations to the wire protocol safely.
Directional Closure
Request and response bodies are independent, so a bidirectional class Protocol::HTTP::Body::Stream can close either direction without implicitly closing the other:
| Operation | Application-level meaning | Typical protocol consequence |
|---|---|---|
read returns nil |
The peer or producer completed the input normally. | The inbound body has reached end-of-stream. |
close_read before end-of-stream |
The application will not consume the remaining input, but may continue writing. | Discard unread data, terminate the exchange, or make the connection non-reusable. |
close_write without an error |
The application has finished producing output, but may continue reading. | Preserve previously written data and send a normal end-of-stream. |
close without an error |
The application has finished with both directions. | Complete or terminate the exchange without reporting an application error. |
close with an error |
The application cannot continue the exchange successfully. | Propagate the error or terminate the exchange using an appropriate protocol error. |
discard |
Consume input without processing it. | Prefer preserving the exchange or connection for reuse. |
These operations update local application-facing state. Returning from a close operation does not guarantee that the peer has observed it or that the underlying transport has been closed synchronously. Those details depend on the protocol and may be completed later.
Initialization
Bodies are typically initialized with the data they need to process. For example:
body = Protocol::HTTP::Body::Buffered.wrap("Hello World")
Reading
Once initialized, bodies can be read in chunks:
body.each do |chunk|
puts "Read #{chunk.bytesize} bytes"
end
Closing
It's important to close bodies when done to release resources:
begin
# ... read from the body ...
rescue => error
# Ignore.
ensure
# The body should always be closed:
body.close(error)
end
Advanced Usage
Rewindable Bodies
Make any body rewindable by buffering:
require "protocol/http/body/rewindable"
# Wrap a non-rewindable body:
file_body = Protocol::HTTP::Body::File.open("data.txt")
rewindable = Protocol::HTTP::Body::Rewindable.new(file_body)
# Read some data:
first_chunk = rewindable.read
# Rewind and read again:
rewindable.rewind
same_chunk = rewindable.read # Same as first_chunk
Head Bodies (Response without content)
For HEAD requests that need content-length but no body:
require "protocol/http/body/head"
# Create head body from another body:
original = Protocol::HTTP::Body::File.open("large_file.zip")
head_body = Protocol::HTTP::Body::Head.for(original)
head_body.length # => size of original file
head_body.read # => nil (no actual content)
head_body.empty? # => true