DEV Community

tengxgfyrz67s
tengxgfyrz67s

Posted on

Prologue-and-Epilogue-Macros

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:

  1. Request arrives — The server receives an HTTP request from a client.
  2. Prologue phase — Initial response data can be sent immediately (headers, status).
  3. Middleware chainrequest_middleware functions process the request.
  4. Route handler — The matched route handler processes the request and builds the response.
  5. Response middlewareresponse_middleware functions process the response.
  6. 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

try_flush and flush

async fn flush_data(stream: &mut Stream) {
    // Try to flush
    stream.try_flush().await;

    // Force flush
    stream.flush().await;
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Use #[try_send] for non-critical data: If losing a data chunk is acceptable (e.g., in streaming), use try_send to avoid crashing on broken connections.

  2. Use #[send] for critical data: When data must be delivered, use send to ensure it goes through.

  3. Always flush before closing: Ensure all buffered data is flushed before closing a connection to avoid data loss.

  4. Combine with Keep-Alive management: Check stream.is_keep_alive() in your epilogue logic to decide whether to keep or close the connection.

  5. Use SSE for real-time updates: The send/flush macros are perfect for Server-Sent Events and other streaming patterns.

  6. 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)