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