Prologue and Epilogue Macros in Hyperlane
Project Code:https://github.com/hyperlane-dev/hyperlane
Introduction
In the hyperlane framework, middleware is the backbone of request processing. While standard request_middleware and response_middleware handle the bulk of request/response processing, hyperlogue also provides specialized prologue and epilogue macros that execute at the very beginning and very end of the request lifecycle. These macros—implemented through the #[try_send], #[send], #[try_flush], and #[flush] attribute macros—give you fine-grained control over when data is sent to the client. This article explores how to use these macros to build sophisticated response pipelines.
Understanding the Request Lifecycle
Before diving into prologue and epilogue macros, it's important to understand the request lifecycle in hyperlane:
- Request arrives — The server receives an HTTP request from a client.
- Prologue phase — Initial response data can be sent immediately (headers, status).
-
Middleware chain —
request_middlewarefunctions process the request. - Route handler — The matched route handler processes the request and builds the response.
-
Response middleware —
response_middlewarefunctions process the response. - Epilogue phase — Final data is flushed to the client, and the connection is managed.
The prologue and epilogue macros control steps 2 and 6, giving you control over when data is actually written to the network stream.
The Send Macros: Prologue Phase
The #[try_send] and #[send] macros control the initial sending of response data. These are particularly useful for streaming responses, where you want to send headers and initial data before the full response body is ready.
Using #[try_send]
The #[try_send] macro attempts to send data through the stream. If the send fails (e.g., due to a broken connection), it handles the error gracefully:
#[try_send]
async fn send_initial_data(stream: &mut Stream) {
// Attempt to send initial response data
stream.try_send("Initial data chunk".as_bytes()).await;
}
Using #[send]
The #[send] macro sends data through the stream and is used when you want to ensure the data is sent:
#[send]
async fn send_response_data(stream: &mut Stream) {
// Send response data
stream.send("Response body".as_bytes()).await;
}
Practical Example: Streaming Response
Here's how you might use send macros to implement a streaming response:
#[route("/api/stream")]
async fn stream_data(ctx: &mut ServerContext) {
let response = ctx.get_mut_response();
response.set_status_code(200);
response.set_header("Content-Type", "text/plain");
response.set_header("Transfer-Encoding", "chunked");
// Send headers immediately
let _ = ctx.stream().await;
// Stream data in chunks
for i in 0..10 {
let chunk = format!("Chunk {}\n", i);
// Use try_send for each chunk
let _ = ctx.stream().await;
}
}
The Flush Macros: Epilogue Phase
The #[try_flush] and #[flush] macros control the final flushing of data to the client. These are essential for ensuring that all buffered data is actually written to the network.
Using #[try_flush]
The #[try_flush] macro attempts to flush buffered data, handling errors gracefully:
#[try_flush]
async fn flush_response(stream: &mut Stream) {
// Attempt to flush any remaining data
stream.try_flush().await;
}
Using #[flush]
The #[flush] macro forces a flush of all buffered data:
#[flush]
async fn flush_all_data(stream: &mut Stream) {
// Flush all buffered data to the client
stream.flush().await;
}
Practical Example: Ensuring Complete Response Delivery
#[route("/api/complete")]
async fn complete_response(ctx: &mut ServerContext) {
let response = ctx.get_mut_response();
response.set_status_code(200);
response.set_body("Complete response data".to_string());
// Ensure all data is flushed to the client
// This is the epilogue phase — making sure nothing is left in the buffer
}
Stream-Level Send and Flush Operations
Beyond the attribute macros, hyperlane provides direct methods on the stream object for sending and flushing data:
try_send and send
async fn manual_send(stream: &mut Stream) {
// Try to send — returns Result
match stream.try_send("data".as_bytes()).await {
Ok(_) => println!("Data sent successfully"),
Err(e) => eprintln!("Send failed: {:?}", e),
}
// Send — more direct
stream.send("more data".as_bytes()).await;
}
try_send_list and send_list
For sending multiple chunks at once:
async fn send_multiple_chunks(stream: &mut Stream) {
let chunks = vec![
"chunk1".as_bytes(),
"chunk2".as_bytes(),
"chunk3".as_bytes(),
];
// Try to send all chunks
stream.try_send_list(&chunks).await;
// Or send all chunks directly
stream.send_list(&chunks).await;
}
try_flush and flush
async fn flush_data(stream: &mut Stream) {
// Try to flush
stream.try_flush().await;
// Force flush
stream.flush().await;
}
Combining Prologue and Epilogue in Middleware
The real power of these macros comes from using them in middleware to create complete response pipelines:
// Prologue middleware — sends headers immediately
#[request_middleware]
async fn prologue_middleware(ctx: &mut ServerContext) {
let response = ctx.get_mut_response();
response.set_header("X-Request-Start", "true");
response.set_header("Server", "hyperlane");
}
// Epilogue middleware — ensures all data is sent
#[response_middleware]
async fn epilogue_middleware(ctx: &mut ServerContext) {
let response = ctx.get_mut_response();
response.add_header("X-Request-Complete", "true");
}
Advanced Pattern: Server-Sent Events (SSE)
The send and flush macros are particularly useful for implementing Server-Sent Events, where data is pushed to the client as it becomes available:
#[route("/api/events")]
async fn server_sent_events(ctx: &mut ServerContext) {
let response = ctx.get_mut_response();
response.set_status_code(200);
response.set_header("Content-Type", "text/event-stream");
response.set_header("Cache-Control", "no-cache");
response.set_header("Connection", "keep-alive");
// Send initial SSE comment to establish the connection
let initial = ":ok\n\n";
// Stream events as they occur
for i in 0..5 {
let event = format!("data: Event {}\n\n", i);
// Each event is sent and flushed immediately
}
}
Connection Management in the Epilogue Phase
The epilogue phase is also where you manage connection lifecycle—deciding whether to keep the connection alive or close it:
async fn manage_connection(stream: &mut Stream) {
if stream.is_keep_alive() {
// Connection will be reused for the next request
// Reset state for next request
let _ = stream.try_get_http_request().await;
} else {
// Close the connection
stream.set_closed(true);
}
}
Practical Pattern: Response Timing Middleware
Here's a complete example that combines prologue and epilogue concepts to measure and log response timing:
use std::time::Instant;
#[request_middleware]
async fn timing_prologue(ctx: &mut ServerContext) {
// Record the start time as an attribute
let start = Instant::now();
ctx.set_attribute("request_start_time", start);
// Set server identification header
let response = ctx.get_mut_response();
response.set_header("Server", "hyperlane");
response.set_header("X-Request-Id", "unique-id");
}
#[response_middleware]
async fn timing_epilogue(ctx: &mut ServerContext) {
// Calculate elapsed time
if let Some(start) = ctx.try_get_attribute::<Instant>("request_start_time") {
let elapsed = start.elapsed();
let response = ctx.get_mut_response();
response.add_header("X-Response-Time", format!("{:?}", elapsed));
}
}
Best Practices
Use
#[try_send]for non-critical data: If losing a data chunk is acceptable (e.g., in streaming), usetry_sendto avoid crashing on broken connections.Use
#[send]for critical data: When data must be delivered, usesendto ensure it goes through.Always flush before closing: Ensure all buffered data is flushed before closing a connection to avoid data loss.
Combine with Keep-Alive management: Check
stream.is_keep_alive()in your epilogue logic to decide whether to keep or close the connection.Use SSE for real-time updates: The send/flush macros are perfect for Server-Sent Events and other streaming patterns.
Handle errors gracefully: Always handle send/flush errors to prevent cascading failures.
Conclusion
Hyperlane's prologue and epilogue macros—#[try_send], #[send], #[try_flush], and #[flush]—provide fine-grained control over the response lifecycle. By understanding when and how to use these macros, you can build sophisticated streaming responses, implement Server-Sent Events, measure response times, and ensure reliable data delivery. Combined with hyperlane's middleware system, these macros give you everything you need to build production-ready HTTP servers with precise control over data flow.
Project Code:https://github.com/hyperlane-dev/hyperlane
Top comments (0)