HyperChad HTML HTTP Renderer
Generic HTTP server integration for HyperChad HTML renderer with framework-agnostic design.
Overview
The HyperChad HTML HTTP Renderer provides:
- Framework Agnostic: Works with any HTTP server implementation
- Request Processing: Process RouteRequest objects into HTTP responses
- Response Generation: Standards-compliant HTTP response generation
- Static Assets: Integrated static file serving
- Action Handling: Server-side action processing
- Error Handling: Comprehensive HTTP error handling
Features
HTTP Server Capabilities
- Generic HTTP: Works with any HTTP server framework
- Request Processing: Process RouteRequest objects into HTTP responses
- Response Building: Generate proper HTTP responses
- Status Codes: Full HTTP status code support
- Headers: Custom header management
- Content Types: Automatic content type detection
HyperChad Integration
- HTML Rendering: Server-side HTML generation
- Routing: URL routing and path matching
- Action Processing: Handle HyperChad actions
- Partial Updates: Support for partial page updates
- Asset Serving: Static asset management
Performance Features
- Async Processing: Fully asynchronous request handling
Installation
Add this to your Cargo.toml:
[dependencies]
hyperchad_renderer_html_http = { path = "../hyperchad/renderer/html/http" }
Usage
Basic HTTP Application
use hyperchad_renderer_html_http::HttpApp;
use hyperchad_renderer_html::DefaultHtmlTagRenderer;
use hyperchad_router::{Router, RouteRequest};
use hyperchad_template::container;
use http::{Request, Response, StatusCode};
use bytes::Bytes;
use qstring::QString;
async fn handle_request(req: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
let router = Router::new();
let tag_renderer = DefaultHtmlTagRenderer::default();
let app = HttpApp::new(tag_renderer, router)
.with_title("My HyperChad App".to_string())
.with_description("A HyperChad HTTP application".to_string())
.with_viewport("width=device-width, initial-scale=1".to_string());
let query_str = req.uri().query().unwrap_or("");
let query = QString::from(query_str).into_iter().collect();
let route_request = RouteRequest {
path: req.uri().path().to_string(),
method: switchy::http::models::Method::from(req.method().as_str()),
query,
headers: req.headers().iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect(),
cookies: std::collections::BTreeMap::new(),
info: hyperchad_router::RequestInfo::default(),
body: Some(std::sync::Arc::new(Bytes::from(req.into_body()))),
};
let response = app.process(&route_request).await?;
Ok(response)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Server};
use std::convert::Infallible;
let make_svc = make_service_fn(|_conn| async {
Ok::<_, Infallible>(service_fn(|req: Request<Body>| async move {
let (parts, body) = req.into_parts();
let body_bytes = hyper::body::to_bytes(body).await.unwrap();
let req = Request::from_parts(parts, body_bytes.to_vec());
match handle_request(req).await {
Ok(response) => {
let (parts, body) = response.into_parts();
Ok::<_, Infallible>(Response::from_parts(parts, Body::from(body)))
}
Err(_) => Ok(Response::builder()
.status(500)
.body(Body::from("Internal Server Error"))
.unwrap()),
}
}))
});
let addr = ([127, 0, 0, 1], 3000).into();
let server = Server::bind(&addr).serve(make_svc);
println!("Server running on http://{}", addr);
server.await?;
Ok(())
}
Advanced Routing
use hyperchad_router::{RouteRequest, Router, RoutePath};
use hyperchad_template::{ContainerVecExt, container};
fn create_router() -> Router {
let router = Router::new();
router.add_route_result("/", |_req| async { Ok::<_, Box<dyn std::error::Error>>(render_home()) });
router.add_route_result("/about", |_req| async { Ok::<_, Box<dyn std::error::Error>>(render_about()) });
router.add_route_result("/users/{id}", |req| async move {
let user_id = req.path.strip_prefix("/users/").unwrap_or("");
Ok::<_, Box<dyn std::error::Error>>(render_user(user_id))
});
router.add_route_result("/api/users", |req| async move { handle_api_users(&req).await });
router
}
fn render_home() -> String {
let view = container! {
html {
head {
title { "Home - HyperChad HTTP" }
meta name="viewport" content="width=device-width, initial-scale=1";
}
body {
div class="container" {
h1 { "Welcome to HyperChad HTTP" }
p { "This is a framework-agnostic HTTP server." }
nav {
a href="/about" { "About" }
" | "
a href="/users/123" { "User Profile" }
" | "
a href="/api/users" { "API" }
}
div class="features" {
h2 { "Features" }
ul {
li { "Framework agnostic design" }
li { "Full HTTP standard support" }
li { "Static asset serving" }
li { "Action processing" }
}
}
}
}
}
};
view.to_string()
}
fn render_user(user_id: &str) -> String {
let view = container! {
html {
head {
title { format!("User {} - HyperChad HTTP", user_id) }
}
body {
div class="container" {
h1 { format!("User Profile: {}", user_id) }
div class="user-info" {
p { format!("User ID: {}", user_id) }
p { "Name: John Doe" }
p { "Email: john@example.com" }
}
a href="/" { "← Back to Home" }
}
}
}
};
view.to_string()
}
async fn handle_api_users(req: &RouteRequest) -> Result<Content, Box<dyn std::error::Error>> {
use hyperchad_renderer::Content;
match req.method.as_ref() {
"GET" => {
let users = serde_json::json!([
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"}
]);
Ok(Content::Json(users))
}
"POST" => {
let body = req.body.as_ref().unwrap();
let new_user: serde_json::Value = serde_json::from_slice(body)?;
let response = serde_json::json!({
"id": 123,
"name": new_user["name"],
"email": new_user["email"]
});
Ok(Content::Json(response))
}
_ => {
Ok(Content::Raw {
data: bytes::Bytes::from_static(b"Method Not Allowed"),
content_type: "text/plain".to_string(),
})
}
}
}
Static Asset Serving
Note: Static asset serving requires the assets feature (enabled by default).
use hyperchad_renderer_html_http::HttpApp;
use hyperchad_renderer_html::DefaultHtmlTagRenderer;
use hyperchad_router::Router;
use hyperchad_renderer::assets::AssetPathTarget;
use hyperchad_template::{ContainerVecExt, container};
use std::path::PathBuf;
fn create_app_with_assets() -> HttpApp<DefaultHtmlTagRenderer> {
let router = Router::new();
let tag_renderer = DefaultHtmlTagRenderer::default();
HttpApp::new(tag_renderer, router)
.with_static_asset_route_handler(|req| {
match req.path.as_str() {
"/css/style.css" => Some(AssetPathTarget::File(PathBuf::from("assets/style.css"))),
"/js/app.js" => Some(AssetPathTarget::File(PathBuf::from("assets/app.js"))),
path if path.starts_with("/images/") => path
.strip_prefix("/images/")
.map(|relative| AssetPathTarget::File(PathBuf::from("assets/images").join(relative))),
path if path.starts_with("/uploads/") => path
.strip_prefix("/uploads/")
.map(|relative| AssetPathTarget::File(PathBuf::from("uploads").join(relative))),
_ => None,
}
})
}
fn render_page_with_assets() -> String {
let view = container! {
html {
head {
title { "HyperChad with Assets" }
link rel="stylesheet" href="/css/style.css";
meta name="viewport" content="width=device-width, initial-scale=1";
}
body {
div class="container" {
h1 { "HyperChad with Static Assets" }
img src="/images/logo.png" alt="Logo" class="logo";
p { "This page includes CSS, JavaScript, and image assets." }
div class="gallery" {
img src="/uploads/photo1.jpg" alt="Photo 1";
img src="/uploads/photo2.jpg" alt="Photo 2";
}
}
script src="/js/app.js" {}
}
}
};
view.to_string()
}
Action Handling
Note: Action handling requires the actions feature (enabled by default).
use hyperchad_renderer_html_http::HttpApp;
use hyperchad_renderer_html::DefaultHtmlTagRenderer;
use hyperchad_router::Router;
use hyperchad_renderer::transformer::actions::logic::Value;
fn create_app_with_actions() -> HttpApp<DefaultHtmlTagRenderer> {
let (action_tx, action_rx) = flume::unbounded();
tokio::spawn(async move {
while let Ok((action_name, value)) = action_rx.recv_async().await {
match action_name.as_str() {
"submit_contact_form" => {
println!("Contact form submitted: {:?}", value);
}
"user_login" => {
println!("Login attempt: {:?}", value);
}
_ => {
println!("Unknown action: {}", action_name);
}
}
}
});
let router = Router::new();
let tag_renderer = DefaultHtmlTagRenderer::default();
HttpApp::new(tag_renderer, router)
.with_action_tx(action_tx)
}
Error Handling
use http::{Request, Response, StatusCode};
use hyperchad_template::{ContainerVecExt, container};
async fn handle_request_with_errors(req: Request<Vec<u8>>) -> Response<Vec<u8>> {
match process_request(req).await {
Ok(response) => response,
Err(e) => handle_error(e),
}
}
fn handle_error(error: Box<dyn std::error::Error>) -> Response<Vec<u8>> {
let (status, message) = match error.downcast_ref::<hyperchad_renderer_html_http::Error>() {
Some(hyperchad_renderer_html_http::Error::Navigate(nav_err)) => {
(StatusCode::NOT_FOUND, format!("Page not found: {}", nav_err))
}
Some(hyperchad_renderer_html_http::Error::IO(io_err)) => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("IO error: {}", io_err))
}
_ => {
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string())
}
};
let error_page = container! {
html {
head {
title { format!("{} - Error", status.as_u16()) }
}
body {
div class="error-page" {
h1 { format!("Error {}", status.as_u16()) }
p { message }
a href="/" { "Go Home" }
}
}
}
};
Response::builder()
.status(status)
.header("Content-Type", "text/html")
.body(error_page.to_string().into_bytes())
.unwrap()
}
Middleware Integration
use http::{Request, Response};
use std::time::Instant;
async fn logging_middleware<F, Fut>(
req: Request<Vec<u8>>,
handler: F,
) -> Response<Vec<u8>>
where
F: FnOnce(Request<Vec<u8>>) -> Fut,
Fut: std::future::Future<Output = Response<Vec<u8>>>,
{
let start = Instant::now();
let method = req.method().clone();
let path = req.uri().path().to_string();
println!("→ {} {}", method, path);
let response = handler(req).await;
let duration = start.elapsed();
let status = response.status();
println!("← {} {} {} {:?}", method, path, status.as_u16(), duration);
response
}
async fn cors_middleware<F, Fut>(
req: Request<Vec<u8>>,
handler: F,
) -> Response<Vec<u8>>
where
F: FnOnce(Request<Vec<u8>>) -> Fut,
Fut: std::future::Future<Output = Response<Vec<u8>>>,
{
let mut response = handler(req).await;
let headers = response.headers_mut();
headers.insert("Access-Control-Allow-Origin", "*".parse().unwrap());
headers.insert("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS".parse().unwrap());
headers.insert("Access-Control-Allow-Headers", "Content-Type, Authorization".parse().unwrap());
response
}
async fn handle_with_middleware(req: Request<Vec<u8>>) -> Response<Vec<u8>> {
cors_middleware(req, |req| {
logging_middleware(req, |req| {
handle_request(req)
})
}).await
}
Feature Flags
Default features: actions, assets, debug, json
actions: Enable server-side action processing (depends on _json and serde)
assets: Enable static asset serving (depends on mime_guess, switchy_async, switchy_fs)
debug: Enable debug-specific functionality
json: Enable JSON content type support (depends on _json)
_json: Internal feature for JSON dependencies (do not enable directly)
HTTP Standards Compliance
Supported Methods
- GET: Retrieve resources
- POST: Create resources
- PUT: Update resources
- DELETE: Delete resources
- PATCH: Partial updates
- HEAD: Headers only
- OPTIONS: CORS preflight
Status Codes
- 2xx Success: 200 OK, 201 Created, 204 No Content
- 3xx Redirection: 301 Moved Permanently, 302 Found, 304 Not Modified
- 4xx Client Error: 400 Bad Request, 401 Unauthorized, 404 Not Found
- 5xx Server Error: 500 Internal Server Error, 503 Service Unavailable
Headers
- Content-Type: Automatic content type detection for assets and responses
- Custom headers can be added via middleware (see middleware example)
Dependencies
Core dependencies:
- http: Standard HTTP types and utilities
- hyperchad_renderer_html: Core HTML rendering functionality
- hyperchad_router: Routing and navigation
- hyperchad_renderer: Renderer abstractions and traits
- thiserror: Error handling
- flume: Multi-producer, multi-consumer channels for action handling
- serde_json: JSON serialization for internal use
Optional dependencies (enabled by features):
- serde: JSON deserialization for actions (enabled by
actions feature)
- mime_guess: Content-type detection for static assets (enabled by
assets feature)
- switchy_async & switchy_fs: Async file I/O for asset serving (enabled by
assets feature)
Integration
This renderer is designed for:
- Custom HTTP Servers: Build your own HTTP server
- Framework Integration: Integrate with existing frameworks
- Microservices: HTTP-based microservices
- API Gateways: Custom API gateway implementations
- Edge Computing: Edge server implementations
Performance Considerations
- Async: Fully asynchronous processing with tokio/async runtime support
- Memory: Efficient memory usage patterns
- File I/O: Async file operations for asset serving