duo v0.1.1
π Why Choose Duo?
Duo is not just another HTTP serverβit's a fully compliant HTTP/2 implementation that brings the power of modern web protocols to Crystal applications. Built from the ground up with performance, compliance, and developer experience in mind.
β¨ Key Features
- π₯ Full HTTP/2 Compliance - Passes all 146 H2Spec tests
- β‘ High Performance - 10,900+ req/s burst, 3,600+ req/s sustained with multiplexing
- π TLS by Default - Built-in SSL/TLS support with ALPN
- π§© Modular Design - Clean handler-based architecture
- π¦ Zero Dependencies - Pure Crystal implementation
- π HTTP/1.1 Fallback - Automatic protocol negotiation
- π‘οΈ Production Ready - Robust error handling and connection management
- π― Developer Friendly - Simple API with powerful features
π Quick Start
Installation
Add this to your application's shard.yml
:
dependencies:
duo:
github: azutoolkit/duo
Run shards install
Hello World Server
require "duo"
class HelloHandler
include Duo::Server::Handler
def call(context : Duo::Server::Context)
context.response << "Hello, HTTP/2 World! π"
context
end
end
# Create SSL context (required for HTTP/2)
ssl_context = OpenSSL::SSL::Context::Server.new
ssl_context.certificate_chain = "cert.pem"
ssl_context.private_key = "key.pem"
ssl_context.alpn_protocol = "h2"
# Start the server
server = Duo::Server.new("::", 9876, ssl_context)
server.listen([HelloHandler.new])
That's it! You now have a fully compliant HTTP/2 server running.
π HTTP/2 Spec Compliance
Duo achieves 100% compliance with the HTTP/2 specification (RFC 7540):
H2Spec Results β
h2spec -h 127.0.0.1 -p 9876 --tls -k
Hypertext Transfer Protocol Version 2 (HTTP/2)
ββ 3. Starting HTTP/2
β ββ 3.5. HTTP/2 Connection Preface
β β ββ 1: Sends client connection preface β
β β ββ 2: Sends invalid connection preface β
β β ββ 3: HTTP/2 connection preface β
β ββ (more tests...)
ββ 4. HTTP Frames
ββ 5. Streams and Multiplexing
ββ 6. Frame Definitions
ββ 7. Error Handling
ββ 8. HTTP Semantics
ββ 9. Additional Requirements
**Result: 146/146 tests passed β
**
Performance Benchmarks π
Burst Performance (100K requests, 1K concurrent)
h2load -n 100000 -c 1000 -m 10 https://localhost:9876/health
finished in 9.17s, 10900.91 req/s, 2.53MB/s
requests: 100000 total, 100000 succeeded, 0 failed
status codes: 100000 2xx
traffic: 23.24MB total, headers savings: 49.48%
Sustained Performance (1M requests, 100 concurrent)
h2load -n 1000000 -c 100 -m 10 https://localhost:9876/health
finished in 276.76s, 3613.26 req/s, 864.46KB/s
requests: 1000000 total, 1000000 succeeded, 0 failed
status codes: 1000000 2xx
traffic: 233.64MB total, headers savings: 48.89%
# Detailed metrics
time for request: 33.22ms - 80.44s (mean: 276.68ms)
time for connect: 8.40ms - 74.37ms (mean: 41.55ms)
time to 1st byte: 90.98ms - 118.22ms (mean: 101.57ms)
π― Real-World Examples
JSON API Server
require "duo"
require "json"
class APIHandler
include Duo::Server::Handler
def call(context : Duo::Server::Context)
request = context.request
response = context.response
case request.path
when "/api/users"
response.headers["content-type"] = "application/json"
response << {
users: [
{id: 1, name: "Alice", email: "alice@example.com"},
{id: 2, name: "Bob", email: "bob@example.com"}
],
total: 2
}.to_json
when "/api/health"
response.headers["content-type"] = "application/json"
response << {status: "healthy", timestamp: Time.utc}.to_json
else
call_next(context)
end
context
end
end
class NotFoundHandler
include Duo::Server::Handler
def call(context : Duo::Server::Context)
context.response.status = 404
context.response.headers["content-type"] = "application/json"
context.response << {error: "Not Found", path: context.request.path}.to_json
context
end
end
# Chain handlers
handlers = [APIHandler.new, NotFoundHandler.new]
server = Duo::Server.new("::", 3000, ssl_context)
server.listen(handlers)
File Server with Caching
class StaticFileHandler
include Duo::Server::Handler
def call(context : Duo::Server::Context)
request = context.request
response = context.response
# Only handle static file requests
unless request.path.starts_with?("/static/")
return call_next(context)
end
file_path = request.path.lchop("/static/")
full_path = File.join("public", file_path)
if File.exists?(full_path) && File.file?(full_path)
# Set appropriate content type
case File.extname(file_path)
when ".html" then response.headers["content-type"] = "text/html"
when ".css" then response.headers["content-type"] = "text/css"
when ".js" then response.headers["content-type"] = "application/javascript"
when ".json" then response.headers["content-type"] = "application/json"
else response.headers["content-type"] = "application/octet-stream"
end
# Add caching headers
response.headers["cache-control"] = "public, max-age=3600"
response.headers["etag"] = %("#{File.info(full_path).modification_time.to_unix}")
# Stream file content
File.open(full_path, "r") do |file|
IO.copy(file, response)
end
else
call_next(context)
end
context
end
end
WebSocket Upgrade (HTTP/1.1)
class WebSocketHandler
include Duo::Server::Handler
def call(context : Duo::Server::Context)
request = context.request
response = context.response
if request.path == "/ws" && request.headers["upgrade"]? == "websocket"
# Handle WebSocket upgrade
response.upgrade("websocket") do |socket|
# WebSocket communication
socket << "Hello WebSocket!"
end
else
call_next(context)
end
context
end
end
π§ͺ Comprehensive Test Server
Duo includes a full-featured test server showcasing all HTTP/2 capabilities:
# Build and run the test server
crystal build examples/test_server.cr -o test_server
./test_server
# Test endpoints
curl -k https://localhost:9876/health # Health check
curl -k https://localhost:9876/api/users # JSON API
curl -k https://localhost:9876/stream/events # Server-sent events
curl -k https://localhost:9876/files/html # Static files
curl -k https://localhost:9876/perf/large # Performance testing
Available Test Endpoints
Endpoint | Purpose | Features |
---|---|---|
/health |
Health monitoring | JSON response with server status |
/api/* |
REST API examples | JSON data, error handling |
/stream/* |
Real-time streaming | SSE, progressive responses |
/files/* |
Static file serving | HTML, CSS, JS, JSON |
/error/* |
Error handling demos | HTTP status codes 400-503 |
/perf/* |
Performance testing | Small to large payloads |
ποΈ Architecture
Handler Chain Pattern
Duo uses a clean, composable handler architecture:
# Handlers are processed in order
handlers = [
AuthHandler.new, # Authentication
CORSHandler.new, # CORS headers
LoggingHandler.new, # Request logging
StaticFileHandler.new, # Static files
APIHandler.new, # API routes
NotFoundHandler.new, # 404 fallback
]
server.listen(handlers)
Each handler can:
- β Process the request and return a response
- β Modify the request/response and pass to the next handler
- β
Short-circuit the chain by not calling
call_next
HTTP/2 Features Implemented
- π Multiplexing - Multiple concurrent requests over single connection
- π¦ Header Compression - HPACK compression reduces overhead
- β‘ Binary Framing - Efficient binary protocol vs text-based HTTP/1.1
- π Server Push - Proactive resource delivery (framework level)
- π Flow Control - Per-stream and connection-level flow control
- π TLS Required - Security by default
- π‘ Stream Prioritization - Request priority management
π HTTP/2 Client
Duo also provides a high-performance HTTP/2 client:
require "duo/client"
client = Duo::Client.new("httpbin.org", 443, tls: true)
# Make concurrent requests
10.times do |i|
headers = HTTP::Headers{
":method" => "GET",
":path" => "/json",
"user-agent" => "duo-client/1.0"
}
client.request(headers) do |response_headers, body|
puts "Response #{i}: #{response_headers[":status"]}"
puts body.gets_to_end
end
end
client.close
Client Features
- β Connection Multiplexing - Reuse connections efficiently
- β Automatic Flow Control - Handles backpressure automatically
- β Stream Management - Concurrent request handling
- β Error Recovery - Robust error handling and reconnection
- β TLS Support - Secure connections with ALPN negotiation
π‘οΈ Production Considerations
SSL/TLS Setup
# Production SSL setup
ssl_context = OpenSSL::SSL::Context::Server.new
ssl_context.certificate_chain = "/path/to/cert.pem"
ssl_context.private_key = "/path/to/private.key"
# Security configurations
ssl_context.verify_mode = OpenSSL::SSL::VerifyMode::PEER
ssl_context.alpn_protocol = "h2"
# Cipher configuration
ssl_context.set_ciphers("ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS")
Performance Tuning
# Environment variables for tuning
ENV["DUO_MAX_CONNECTIONS"] = "1000"
ENV["DUO_WORKER_THREADS"] = "8"
ENV["DUO_BUFFER_SIZE"] = "8192"
# Connection pooling
server = Duo::Server.new(host, port, ssl_context)
server.max_connections = 1000
server.keepalive_timeout = 30.seconds
Monitoring and Logging
# Configure logging
Log.for("Duo").level = Log::Severity::Info
class MetricsHandler
include Duo::Server::Handler
def call(context : Duo::Server::Context)
start_time = Time.monotonic
result = call_next(context)
duration = Time.monotonic - start_time
status = context.response.status
Log.info { "#{context.request.method} #{context.request.path} #{status} #{duration.total_milliseconds}ms" }
result
end
end
π Comparison with Other Servers
Feature | Duo | nginx | Apache | Node.js |
---|---|---|---|---|
HTTP/2 Full Compliance | β 146/146 | β | β | β |
Pure Language Implementation | β Crystal | β C | β C | β C++ |
Memory Safety | β | β | β | β |
Built-in TLS | β | β | β | β |
Multiplexing | β | β | β | β |
Zero Dependencies | β | β | β | β |
Development Speed | β‘ Fast | π Slow | π Slow | β‘ Fast |
π§ Advanced Usage
Custom Error Handling
class ErrorHandler
include Duo::Server::Handler
def call(context : Duo::Server::Context)
call_next(context)
rescue ex : Exception
context.response.status = 500
context.response.headers["content-type"] = "application/json"
context.response << {
error: "Internal Server Error",
message: ex.message,
timestamp: Time.utc.to_rfc3339
}.to_json
context
end
end
Middleware Pattern
def with_timing(handler)
->(context : Duo::Server::Context) {
start = Time.monotonic
result = handler.call(context)
duration = Time.monotonic - start
context.response.headers["x-response-time"] = "#{duration.total_milliseconds}ms"
result
}
end
Rate Limiting
class RateLimitHandler
include Duo::Server::Handler
def initialize(@limit : Int32 = 100, @window : Time::Span = 1.minute)
@requests = {} of String => Array(Time)
end
def call(context : Duo::Server::Context)
client_ip = context.request.headers["x-forwarded-for"]? || "unknown"
now = Time.utc
@requests[client_ip] ||= [] of Time
@requests[client_ip] = @requests[client_ip].select { |time| now - time < @window }
if @requests[client_ip].size >= @limit
context.response.status = 429
context.response.headers["retry-after"] = @window.total_seconds.to_i.to_s
context.response << "Rate limit exceeded"
return context
end
@requests[client_ip] << now
call_next(context)
end
end
π€ Contributing
We welcome contributions! Duo is designed to be approachable for developers of all levels.
Getting Started
- Fork the repository
- Clone your fork:
git clone https://github.com/yourusername/duo.git
- Install dependencies:
shards install
- Run tests:
crystal spec
- Build examples:
crystal build examples/test_server.cr
Development Workflow
# Run the full test suite
crystal spec
# Run H2Spec compliance tests
h2spec -h 127.0.0.1 -p 9876 --tls -k
# Performance benchmarking
h2load -n 10000 -c 10 https://127.0.0.1:9876/
# Code formatting
crystal tool format
What We Need
- π Documentation - Examples, guides, API docs
- π§ͺ Tests - More test coverage, edge cases
- π Performance - Optimizations, benchmarks
- π§ Features - Server push, WebSocket integration
- π Bug Reports - Real-world usage feedback
π Resources
- π HTTP/2 RFC 7540
- π§ͺ H2Spec Testing Tool
- β‘ H2Load Benchmarking
- π Crystal Language
- π Examples Directory
π License
Duo is released under the MIT License. Feel free to use it in both open source and commercial projects.
Built with β€οΈ by the Crystal community
β Star us on GitHub β’ π Report Bug β’ π¬ Discussions
duo
- 83
- 4
- 3
- 0
- 1
- 2 days ago
- April 26, 2021
MIT License
Tue, 15 Jul 2025 11:54:14 GMT