duo v0.1.1

A Pure HTTP 2 Server for the Crystal Language with full support for H2Load and H2Spec test coverage
Duo HTTP/2 Server

πŸš€ Duo HTTP/2 Server

A high-performance, fully compliant HTTP/2 server written in Crystal

Crystal CI Codacy Badge H2Spec Compliant Performance

🌟 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

  1. Fork the repository
  2. Clone your fork: git clone https://github.com/yourusername/duo.git
  3. Install dependencies: shards install
  4. Run tests: crystal spec
  5. 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

πŸ“œ 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

Repository

duo

Owner
Statistic
  • 83
  • 4
  • 3
  • 0
  • 1
  • 2 days ago
  • April 26, 2021
License

MIT License

Links
Synced at

Tue, 15 Jul 2025 11:54:14 GMT

Languages