Responses

Responses

On this page

A handler can return a Response, or anything that becomes one.

async fn index() -> Response {
    Response::json(&posts)
}

async fn health() -> &'static str {
    "ok"
}

Constructors

ConstructorStatusContent-Type
Response::ok(body)200none set
Response::text(s)200text/plain; charset=utf-8
Response::html(s)200text/html; charset=utf-8
Response::json(&v)200application/json
Response::no_content()204—
Response::created(location)201with Location
Response::download(bytes, name)200with Content-Disposition: attachment
Response::stream(s)200streamed body
Response::new(status)yours—
Response::json(&post).with_status(StatusCode::CREATED)
Response::created("/posts/hello")
Response::download(pdf_bytes, "invoice.pdf")

Modifiers

Every modifier consumes and returns the response, so they chain:

Response::json(&post)
    .with_status(StatusCode::CREATED)
    .with_header("x-request-id", &id)
    .with_added_header("vary", "accept")
    .with_cookie(&Cookie::new("seen", "1"))
Method
.with_status(s)
.with_body(b)
.with_header(n, v)replaces
.with_added_header(n, v)appends — for Set-Cookie, Vary, Link
.with_content_type(t)
.with_cookie(&c)
.without_cookie(name)sends a removal cookie

And to read one back — which is most of what a test does:

response.status()
response.header("location")
response.headers()
response.body()
response.is_successful()          // 2xx
response.extensions()

response.into_bytes().await?      // consumes it — a stream can only be read once
response.into_string().await?
response.into_json::<PostView>().await?

into_json quotes the start of the body in its error, because a parse failure is nearly always an error response nobody expected, and expected value at line 1 column 1 says nothing about which one.

In a test, reach for TestResponse instead — it reads the body once and lets every assertion chain.

take_body() takes the body and leaves an empty one behind, for middleware that has to look at what was produced and put something else back — compressing it, hashing it for an ETag.

IntoResponse

Handlers rarely build a Response by hand, because these all convert:

TypeResult
Responseitself
()200, empty
StatusCodethat status, empty
String, &'static str200 text/plain
Html<T>200 text/html
Json<T> where T: Serialize200 application/json
serde_json::Value200 application/json
Redirect302/301/303/307
Body200 with that body
(StatusCode, T)T's response with that status
Option<T>T's response, or a 404
Result<T, E: Into<Error>>T's response, or the error's
Errorsee Error Handling
async fn create() -> (StatusCode, Json<Post>) {
    (StatusCode::CREATED, Json(post))
}

async fn show(id: u64) -> Option<Json<Post>> {
    find(id).map(Json)          // None becomes a 404
}

Implement it for your own types when a handler returns something domain-shaped:

impl IntoResponse for PostView {
    fn into_response(self) -> Response {
        Response::json(&self).with_header("x-post-id", &self.id.to_string())
    }
}

Redirects

Redirect::to("/posts")            // 302 Found
Redirect::permanent("/new")       // 301
Redirect::see_other("/posts")     // 303 — after a POST
Redirect::temporary("/retry")     // 307 — preserves the method

see_other is the one you want after a successful form submission: it turns the follow-up into a GET, which is what stops a refresh from re-submitting.

Cookies

use rainier_framework::http::{Cookie, SameSite};

let cookie = Cookie::new("session", &id)
    .path("/")
    .domain("example.com")
    .max_age(3600)
    .secure(true)
    .http_only(true)
    .same_site(SameSite::Lax);

Response::no_content().with_cookie(&cookie)

Cookie::new starts http_only and SameSite::Lax, because the common case is a session cookie and those are the settings it should have. Turn them off deliberately if you have a reason.

Removal:

Response::no_content().without_cookie("session")
// or
Cookie::removal("session")

Streaming

A Body is either bytes or a stream, so a large response does not have to fit in memory:

use futures::stream;

let stream = stream::iter(chunks.into_iter().map(Ok));
Response::stream(stream).with_content_type("text/event-stream")

This is the asymmetry with buffered request bodies: requests are bounded by a limit you set and benefit from synchronous accessors; responses are unbounded and benefit from streaming.

To collect one in a test:

let bytes = response.into_http().into_body().collect().await?;
let text = String::from_utf8(bytes.to_vec())?;

Views

let html = View::instance().render("posts.show", &json!({ "post": post }))?;
Ok(Response::html(html))

See Views.

Errors

Returning Err from a handler produces a response through IntoResponse for Error — status from the ErrorKind, body shaped by whether the client expects JSON. The kernel may then re-render it through the ExceptionRenderer.

async fn show(request: Req) -> Result<Response> {
    let post = posts.find_or_fail(id).await?;      // 404, with a message naming the model
    Ok(Response::json(&post))
}

This page is the framework’s docs/responses.md at 7b8cb15. Found a mistake? Edit it on GitHub.

Type to search 2.0.0 docs.