Response Building and Sending in Hyperlane
Project Code:https://github.com/hyperlane-dev/hyperlane
Introduction
In the hyperlane framework, response building and sending are core operations that every developer must master. Unlike many web frameworks that abstract these details away, hyperlane gives you fine-grained control over how HTTP responses are constructed and delivered to clients. This article explores the comprehensive API for building responses, setting headers, managing status codes, and efficiently sending data back to clients.
Hyperlane is a lightweight, high-performance, cross-platform Rust HTTP server library built on top of Tokio. Its response system is designed for both simplicity and flexibility, supporting method chaining, attribute macros, and multiple sending strategies.
Setting Response Basics
HTTP Version and Status Code
Every HTTP response starts with a version and a status code. Hyperlane provides a fluent API for setting these fundamental properties:
ctx.get_mut_response().set_version(HttpVersion::Http1_1).set_status_code(200);
This sets the response to HTTP/1.1 with a 200 OK status. The HttpVersion enum supports both HTTP/1.1 and HTTP/1.0, giving you control over protocol negotiation.
Response Body
The response body can be set with a simple string:
ctx.get_mut_response().set_body("Hello World");
This stores the body content internally in the response object. The body is not sent to the client until you explicitly call the build() method and send the resulting data through the stream.
Response Headers
Hyperlane provides two methods for setting response headers:
ctx.get_mut_response().set_header(CONTENT_TYPE, APPLICATION_JSON);
ctx.get_mut_response().add_header(SERVER, "hyperlane");
-
set_header— Sets a header value, replacing any existing value for the same key. -
add_header— Appends a header value, allowing multiple values for the same header key (useful forSet-Cookieheaders).
These methods use the HeaderName constants defined in hyperlane's HTTP module, ensuring type safety and preventing typos.
Building the Response
The build() Method
The build() method serializes the response into a byte buffer that can be sent over the network:
let data = ctx.get_mut_response().build();
This method takes all the configured properties — version, status code, headers, and body — and produces a complete HTTP response in binary format. The resulting data is ready to be sent through the stream.
It is important to note that build() should be called after all response properties have been configured. Any changes made to the response after calling build() will not be reflected in the already-built data.
Sending the Response
Hyperlane offers multiple strategies for sending response data, each with different trade-offs:
try_send — Non-Blocking Send
stream.try_send(data).await;
try_send attempts to send the data without blocking. If the stream's internal buffer is full, it returns an error immediately rather than waiting. This is useful when you want to handle backpressure gracefully:
let data = ctx.get_mut_response().build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
return Status::Reject;
}
send — Blocking Send
stream.send(data).await;
send will wait until the data can be sent, blocking the current task if necessary. Use this when you need guaranteed delivery and can tolerate potential waiting.
try_send_list — Batch Sending
stream.try_send_list(&frame_list).await;
For scenarios where you need to send multiple frames at once (such as WebSocket messages or SSE events), try_send_list efficiently sends a collection of data frames in a single operation.
try_flush — Flushing the Stream
stream.try_flush().await;
After sending data, calling try_flush ensures that all buffered data is pushed to the underlying socket immediately. This is particularly important for SSE and streaming responses where data needs to reach the client without delay.
Attribute Macros for Response Configuration
Hyperlane's attribute macro system provides a declarative approach to response configuration, making your code more concise and readable.
Setting Status Code and Version
#[response_status_code(200)]
#[response_version(HttpVersion::Http1_1)]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
// Response is automatically configured with 200 OK and HTTP/1.1
Status::Continue
}
Setting Response Body
#[response_body("Hello World")]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
// The response body is automatically set to "Hello World"
Status::Continue
}
Setting Response Headers
#[response_header(SERVER => HYPERLANE)]
#[response_header(SET_COOKIE, "session_id=abc123")]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
// Headers are automatically added to the response
Status::Continue
}
Clearing Response Headers
#[clear_response_headers]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
// All existing response headers are cleared
Status::Continue
}
Attribute Macros for Sending
Hyperlane also provides attribute macros for the sending phase:
#[try_send]
#[send]
#[try_flush]
#[flush]
#[closed]
These macros can be applied to handler methods to automatically perform the corresponding stream operation after the handler completes. For example, #[try_send] will automatically build and send the response through the stream.
Complete Response Lifecycle Example
Here is a complete example showing the full response lifecycle in a custom middleware:
struct ResponseMiddleware;
impl ServerHook for ResponseMiddleware {
async fn new(_: &mut Stream, _: &mut Context) -> Self {
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
// Step 1: Configure the response
ctx.get_mut_response()
.set_version(HttpVersion::Http1_1)
.set_status_code(200);
// Step 2: Set the body
ctx.get_mut_response().set_body("Hello World");
// Step 3: Add headers
ctx.get_mut_response().set_header(CONTENT_TYPE, APPLICATION_JSON);
ctx.get_mut_response().add_header(SERVER, "hyperlane");
// Step 4: Build the response into bytes
let data = ctx.get_mut_response().build();
// Step 5: Send the response, handling potential errors
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
return Status::Reject;
}
Status::Continue
}
}
server.response_middleware::<ResponseMiddleware>();
Combining Response Building with Other Features
Response Building in Route Handlers
Response building integrates seamlessly with route handlers. You can use route parameters and request data to construct dynamic responses:
#[route("/test/{text}")]
struct Route;
impl ServerHook for Route {
async fn new(_: &mut Stream, _: &mut Context) -> Self {
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let param: String = ctx.get_route_param("text");
let body = format!("Hello, {}!", param);
ctx.get_mut_response()
.set_version(HttpVersion::Http1_1)
.set_status_code(200)
.set_body(body);
let data = ctx.get_mut_response().build();
stream.try_send(data).await;
Status::Continue
}
}
Response Building with Cookies
Response building works hand-in-hand with cookie management. After building a cookie with CookieBuilder, you attach it to the response headers:
let cookie = CookieBuilder::new("session_id", "abc123")
.set_path("/")
.http_only()
.build();
ctx.get_mut_response().set_header(SET_COOKIE, &cookie);
Response Building with SSE
For Server-Sent Events, the response building process is slightly different. You set the content type to text/event-stream and initialize with an empty body:
let data = ctx.get_mut_response()
.set_header(CONTENT_TYPE, TEXT_EVENT_STREAM)
.set_body(Vec::new())
.build();
stream.try_send(data).await;
for i in 0..10 {
let body = format!("data:{i}{HTTP_DOUBLE_BR}");
stream.try_send(&body).await;
}
Best Practices
Always call
build()after all properties are configured. Changes made afterbuild()are not reflected in the serialized data.Use
try_sendfor non-critical responses where you can gracefully handle backpressure. Usesendwhen guaranteed delivery is required.Prefer attribute macros for simple response configurations to keep your code clean and maintainable.
Handle send errors gracefully. Always check the return value of
try_sendand close the stream if necessary usingstream.set_closed(true).Flush after sending streaming data. For SSE and long-polling scenarios, call
try_flush()to ensure data reaches the client promptly.Set appropriate headers before building. Remember that the order of header operations matters —
set_headerreplaces whileadd_headerappends.
Conclusion
Hyperlane's response building and sending system provides a powerful and flexible foundation for constructing HTTP responses. Whether you use the fluent method chain API or the declarative attribute macros, you have full control over every aspect of the response lifecycle. The multiple sending strategies — try_send, send, try_send_list, and try_flush — ensure that you can handle everything from simple request-response patterns to complex streaming scenarios with ease.
By mastering these APIs, you can build robust, high-performance web applications that take full advantage of hyperlane's speed and efficiency.
Project Code:https://github.com/hyperlane-dev/hyperlane
Top comments (0)