DEV Community

tengxgfyrz67s
tengxgfyrz67s

Posted on

Request-and-Response-Deep-Dive

Request and Response Deep Dive

Project Code:https://github.com/hyperlane-dev/hyperlane

At the heart of every HTTP server lies the processing of incoming requests and the generation of outgoing responses. Hyperlane provides a comprehensive, ergonomic API for both sides of this equation. This article takes a deep dive into how hyperlane handles requests and responses, covering everything from basic header access to JSON body parsing, cookies, and attribute macros.

Understanding the Request Lifecycle

When a client sends an HTTP request to a hyperlane server, the following sequence occurs:

  1. The TCP connection is accepted and a Stream is created.
  2. The request is parsed from the stream into a structured Request object.
  3. The request passes through any registered request middleware.
  4. The request is matched against registered routes.
  5. The matched route's handle method is called.
  6. The response passes through any registered response middleware.
  7. The response is serialized and sent back to the client.

Understanding this lifecycle is crucial for writing effective middleware and route handlers.

Accessing Request Data

Hyperlane provides multiple ways to access request data — through method calls and through attribute macros.

Method-Based Access

The Context object gives you access to all aspects of the incoming request:

let method = ctx.get_request().get_method();
let path = ctx.get_request().get_path();
let host = ctx.get_request().get_host();
let headers = ctx.get_request().get_headers();
let body = ctx.get_request().get_body_string();
let json_body: T = ctx.get_request().get_body_json::<T>();
let query = ctx.get_request().try_get_query("key");
Enter fullscreen mode Exit fullscreen mode

Let's examine each of these:

  • get_method() — Returns the HTTP method (GET, POST, PUT, DELETE, etc.).
  • get_path() — Returns the URL path portion of the request.
  • get_host() — Returns the Host header value.
  • get_headers() — Returns all request headers.
  • get_body_string() — Returns the raw request body as a string.
  • get_body_json::<T>() — Deserializes the request body as JSON into a type T. This requires T to implement serde::Deserialize.
  • try_get_query("key") — Attempts to extract a query parameter by name.

Attribute Macro Access

Hyperlane also provides attribute macros that automatically extract request data and bind it to variables:

#[request_body(body)]
#[request_body_json(body: TestData)]
#[request_header(HOST => host_value)]
#[try_get_request_header(HOST => host_value)]
#[request_path(path)]
#[request_query("key" => query_value)]
#[is_get_method]
#[methods("GET", "POST")]
#[is_http1_1_version]
#[is_ws_upgrade_type]
Enter fullscreen mode Exit fullscreen mode

These macros are applied to the handle method of your route handler. For example:

#[route("/test/{text}")]
struct Route;
impl ServerHook for Route {
    async fn new(_: &mut Stream, _: &mut Context) -> Self { Self }

    #[request_body_json(body: TestData)]
    #[request_query("key" => query_value)]
    async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
        // `body` is now a TestData instance deserialized from the request body
        // `query_value` is the value of the "key" query parameter
        Status::Continue
    }
}
Enter fullscreen mode Exit fullscreen mode

The attribute macros support:

  • #[request_body(body)] — Binds the raw request body string to body.
  • #[request_body_json(body: TestData)] — Deserializes the JSON body into a TestData struct.
  • #[request_header(HOST => host_value)] — Extracts the HOST header into host_value.
  • #[try_get_request_header(HOST => host_value)] — Safely attempts to extract a header.
  • #[request_path(path)] — Binds the request path to path.
  • #[request_query("key" => query_value)] — Extracts a query parameter.
  • #[is_get_method] — Checks if the request method is GET.
  • #[methods("GET", "POST")] — Checks if the request method matches one of the specified methods.
  • #[is_http1_1_version] — Checks if the request uses HTTP/1.1.
  • #[is_ws_upgrade_type] — Checks if the request is a WebSocket upgrade request.

Route Parameters

Dynamic route parameters are a core feature of any web framework. Hyperlane uses the {param_name} syntax in route paths:

server.route::<Route>("/test/{text}");
server.route::<Route>("/test/{number:\\d+}");
Enter fullscreen mode Exit fullscreen mode

Route parameters can be accessed through the Context:

let param: Option<String> = ctx.try_get_route_param("text");
let param: String = ctx.get_route_param("text");
let params = ctx.get_route_params();
Enter fullscreen mode Exit fullscreen mode
  • try_get_route_param("text") — Returns Option<String>, safe for optional parameters.
  • get_route_param("text") — Returns String, panics if the parameter doesn't exist.
  • get_route_params() — Returns all route parameters as a collection.

For regex-constrained parameters like {number:\\d+}, the route will only match if the URL segment satisfies the regex pattern.

Route Filters

Hyperlane supports route-level filtering through attribute macros:

#[host("example.com")]
#[reject_host("blocked.com")]
#[referer("https://example.com")]
#[reject_referer("https://malicious.com")]
#[filter(ctx.get_request().get_method() == &RequestMethod::Get)]
#[reject(ctx.get_request().get_path().len() > 1000)]
Enter fullscreen mode Exit fullscreen mode

These filters allow you to:

  • #[host("example.com")] — Only match requests to a specific host.
  • **#[reject_host("blocked.com")] — Reject requests to a specific host.
  • #[referer("https://example.com")] — Only match requests from a specific referer.
  • #[reject_referer("https://malicious.com")] — Reject requests from a specific referer.
  • #[filter(...)] — Apply a custom filter expression.
  • #[reject(...)] — Apply a custom rejection expression.

Building Responses

Hyperlane's Context provides mutable access to the response object:

ctx.get_mut_response().set_version(HttpVersion::Http1_1).set_status_code(200);
ctx.get_mut_response().set_body("Hello World");
ctx.get_mut_response().set_header(CONTENT_TYPE, APPLICATION_JSON);
ctx.get_mut_response().add_header(SERVER, "hyperlane");
Enter fullscreen mode Exit fullscreen mode

Key response methods:

  • set_version(HttpVersion::Http1_1) — Sets the HTTP version.
  • set_status_code(200) — Sets the HTTP status code.
  • set_body("Hello World") — Sets the response body.
  • set_header(CONTENT_TYPE, APPLICATION_JSON) — Sets a response header (replaces existing value).
  • add_header(SERVER, "hyperlane") — Adds a response header (allows multiple values).

Attribute Macro Responses

You can also configure responses using attribute macros:

#[response_status_code(200)]
#[response_version(HttpVersion::Http1_1)]
#[response_body("Hello World")]
#[response_header(SERVER => HYPERLANE)]
#[response_header(SET_COOKIE, "session_id=abc123")]
#[clear_response_headers]
Enter fullscreen mode Exit fullscreen mode

These macros are applied to the handle method:

#[response_status_code(200)]
#[response_body("Hello World")]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
    // The response is automatically configured with status 200 and body "Hello World"
    Status::Continue
}
Enter fullscreen mode Exit fullscreen mode

Working with Cookies

Cookies are essential for session management and stateful web applications. Hyperlane provides comprehensive cookie support:

Reading Cookies

let cookies = ctx.get_request().try_get_cookies();
let cookie = ctx.get_request().try_get_cookie("session_id");
Enter fullscreen mode Exit fullscreen mode
  • try_get_cookies() — Returns all cookies from the request.
  • try_get_cookie("session_id") — Returns a specific cookie by name.

Setting Cookies with CookieBuilder

Hyperlane provides the CookieBuilder for constructing cookies with various attributes:

let cookie = CookieBuilder::new("session_id", "abc123").set_path("/").http_only().build();
ctx.get_mut_response().set_header(SET_COOKIE, &cookie);
Enter fullscreen mode Exit fullscreen mode

The CookieBuilder supports:

let cookie = CookieBuilder::new("session", "token123")
    .set_expires("Wed, 21 Oct 2025 07:28:00 GMT")
    .set_domain("example.com")
    .set_same_site("Strict")
    .set_max_age(3600)
    .set_path("/")
    .secure()
    .http_only()
    .build();
Enter fullscreen mode Exit fullscreen mode

Available builder methods:

  • set_expires(...) — Sets the cookie expiration date.
  • set_domain(...) — Sets the cookie domain.
  • set_same_site(...) — Sets the SameSite attribute (Strict, Lax, None).
  • set_max_age(...) — Sets the cookie max age in seconds.
  • set_path(...) — Sets the cookie path.
  • secure() — Marks the cookie as Secure (HTTPS only).
  • http_only() — Marks the cookie as HttpOnly (inaccessible to JavaScript).

Clearing Cookies

To clear a cookie, create one with an empty value and max age of 0:

let clear_cookie = CookieBuilder::new("session", "").set_max_age(0).build();
Enter fullscreen mode Exit fullscreen mode

Sending Responses

Once you've built a response, you need to send it to the client:

let data = ctx.get_mut_response().build();
stream.try_send(data).await;
stream.send(data).await;
stream.try_send_list(&frame_list).await;
stream.try_flush().await;
Enter fullscreen mode Exit fullscreen mode
  • build() — Serializes the response into bytes.
  • stream.try_send(data).await — Attempts to send data, returns an error if the stream is closed.
  • stream.send(data).await — Sends data, blocking until complete.
  • stream.try_send_list(&frame_list).await — Sends multiple frames at once.
  • stream.try_flush().await — Flushes any buffered data.

Attribute Macros for Sending

#[try_send]
#[send]
#[try_flush]
#[flush]
#[closed]
Enter fullscreen mode Exit fullscreen mode

These macros handle the send/flush operations automatically:

#[try_send]
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
    // The response is automatically built and sent
    Status::Continue
}
Enter fullscreen mode Exit fullscreen mode

Common Patterns

JSON API Response

ctx.get_mut_response()
    .set_header(CONTENT_TYPE, APPLICATION_JSON)
    .set_body(json_string);
let data = ctx.get_mut_response().build();
stream.try_send(data).await;
Enter fullscreen mode Exit fullscreen mode

Authentication Middleware

impl ServerHook for AuthMiddleware {
    async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
        let auth_str = ctx.get_request().try_get_header_back(AUTHORIZATION).unwrap_or_default();
        if auth_str.is_empty() {
            let data = ctx.get_mut_response().set_status_code(401).set_body("Unauthorized").build();
            if stream.try_send(data).await.is_err() {
                stream.set_closed(true);
            }
            return Status::Reject;
        }
        Status::Continue
    }
}
Enter fullscreen mode Exit fullscreen mode

CORS Middleware

ctx.get_mut_response()
    .set_header(ACCESS_CONTROL_ALLOW_ORIGIN, WILDCARD_ANY)
    .set_header(ACCESS_CONTROL_ALLOW_METHODS, ALL_METHODS)
    .set_header(ACCESS_CONTROL_ALLOW_HEADERS, WILDCARD_ANY);
Enter fullscreen mode Exit fullscreen mode

Conclusion

Hyperlane provides a rich, dual-mode API for handling requests and responses. Whether you prefer the explicit method-call style or the concise attribute macro style, you have full control over every aspect of the HTTP request/response cycle. The combination of route parameters, filters, cookie management, and flexible response building makes hyperlane a powerful tool for building web applications of any complexity.


Project Code:https://github.com/hyperlane-dev/hyperlane

Top comments (0)