Protocol::HTTPGuidesMessage Body

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:

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

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