Every Suprnova handler returns a Response, which is an alias for
Result<HttpResponse, HttpResponse>. The Ok arm carries the success
response, the Err arm carries an already-rendered error response, and
the ? operator collapses any error type that has a From into
HttpResponse along the way. This chapter is the practical reference
for building the Ok side - the HttpResponse builders, the
Redirect builder, the cookie API, and the abort_* short-circuits.
For the error story see Error Model and
Error Handling.
HttpResponse builders
HttpResponse is the wire-shaped response type. The constructors set
sensible defaults; the chainable setters override them.
Body constructors
use ;
use json;
pub async
Two streaming constructors exist for long-lived responses:
HttpResponse::sse(stream)- Server-Sent Events. Wraps aStreamofSseEventvalues, sets the four required headers (Content-Type: text/event-stream,Cache-Control: no-cache,Connection: keep-alive,X-Accel-Buffering: no), and keeps the connection open until the producing stream ends. See Server-Sent Events.HttpResponse::stream_bytes(stream)- generic chunked response. Takes aStream<Item = Result<Bytes, Infallible>>. The error type isInfallibleby design: every producer in the framework turns its own errors into a terminal stream message before the stream ends, because there is no way to surface a transport-level error to the client mid-response.
Status, headers, cookies
Every builder returns Self, so chain freely:
use ;
use json;
pub async
| Method | Behavior |
|---|---|
.status(code) |
Set the HTTP status. Codes outside 100..=599 downgrade to 500 at the wire boundary with a warning log. |
.header(name, value) |
Append a header. Duplicates allowed (matches Set-Cookie semantics). |
.replace_header(name, value) |
Drop any prior occurrences and set one. |
.with_headers([(k, v), ...]) |
Append many at once. Accepts any IntoIterator<Item = (K, V)>. |
.without_header(name) |
Remove every occurrence (case-insensitive). |
.header_value(name) |
Read back the first-set value. Useful in tests. |
.cookie(Cookie) |
Attach one cookie as Set-Cookie. |
.with_cookies([Cookie, ...]) |
Attach many. |
.without_cookie(name) |
Schedule a deletion (equivalent to Cookie::forget(name)). |
The same chainable setters are available on a Response (the
Result) through the ResponseExt trait, so the macros stay
ergonomic:
use ;
pub async
ResponseExt exposes .status, .header, .with_headers,
.without_header, .cookie, .with_cookies, and .without_cookie.
Wire-boundary validation
HttpResponse::into_hyper runs two safety filters before handing the
response to hyper:
- Status range. Anything outside
100..=599downgrades to 500 with atracing::warn!. This catchesAppError::status(700)typos at the boundary instead of letting non-conformant codes reach the wire. - Header CRLF injection. Every header name and value is validated
via hyper's own
HeaderName::try_from/HeaderValue::try_from. Any rejected header is dropped with a warn log and the response is built without it. Attacker-controlled values that get reflected into a header (CORS allow-headers,X-Forwarded-*, custom debug headers) cannot split the response.
Both filters are silent in the success path - you only see them in logs when something tried to slip through.
Response macros
Two Response-shaped macros exist for the common cases:
use ;
pub async
pub async
Both expand to Ok(HttpResponse::...). Chain ResponseExt setters on
either to adjust status, headers, or cookies.
Cookies
Cookie::new(name, value) produces a cookie with secure defaults -
HttpOnly, Secure, SameSite=Lax, Path=/. Override per cookie:
use Cookie;
use Duration;
let session = new
.http_only
.secure
.same_site
.path
.domain
.max_age
.partitioned;
Three convenience constructors cover common patterns:
Cookie::forget(name)- empty value,Max-Age=0. Use this on logout to instruct the browser to drop the cookie.Cookie::forever(name, value)- five-yearMax-Age.Cookie::encrypted(name, plaintext)- AES-256-GCM ciphertext bound to theCryptPurpose::CookieAAD so cookie ciphertext cannot be replayed into another framework surface (cursors, 2FA secrets, casts). RequiresAPP_KEYto be set at boot. The companionCookie::read_encrypted(wire)decrypts a value produced by the same path. See Encryption.
Header serialization percent-encodes every byte that isn't a valid cookie-octet per RFC 6265, including all control characters. CRLF in a cookie name or value gets encoded, not propagated - header injection through cookies is closed at the serializer.
Redirects
Redirect covers the full Laravel redirector surface. Every variant
implements From<Redirect> for Response, so the idiomatic form is
Redirect::...().into().
Targets
use ;
// Explicit URL or path
let _ = to;
// Same thing, slightly shorter free function
let _ = redirect_to;
// Named route (returns RedirectRouteBuilder)
let _ = route.with;
// Explicit external URL - same as `to`, but the name signals
// "this is going off-site" for open-redirect audits
let _ = away;
// Refresh the page (reads previous URL from the session; falls back
// to "/" if no session scope is active)
let _ = refresh;
// Same, but taking an explicit Request when no scope is active
// let _ = Redirect::refresh_for(&request);
// Session previous_url, with fallback when no session is in scope
let _ = back;
// Session-stored intended URL, consumed on read, with fallback
let _ = intended;
// Guest redirect: stashes the current request URL as "intended" and
// sends the user to a login page
// let _ = Redirect::guest(&request, "/login");
Redirect::back, Redirect::intended, Redirect::guest, and
Redirect::refresh all integrate with the session. Without a session
scope they fall through to their defaults silently - handy for
partial test setups. See Session.
Named-route validation
The redirect! proc-macro validates the route name at compile time
and expands to Redirect::route(name):
use ;
pub async
Status codes
use Redirect;
let _ = to.permanent; // 301
let _ = to.status; // 303, 307, 308, ...
The default is 302.
Flash data
Redirect builders carry their own flash bag. On conversion to a
Response the bag drains into the live session, surviving exactly
one more request:
use Redirect;
let _ = back
.with // single key/value
.with_input
.with_errors
.with_errors_bag;
The receiving page reads these back through session.get(...) (for
with), session.get_old_input(...) (for with_input), and the
bag map drained by session.pull_errors_flash() (for
with_errors / with_errors_bag). The Inertia layer consumes the
errors-flash automatically - every Inertia response's errors prop
is seeded from the session, so Redirect::back().with_errors(...)
surfaces messages on the destination without extra wiring. The
X-Inertia-Error-Bag request header scopes the prop under a named
bag for multi-form pages.
Note that on RedirectRouteBuilder (what Redirect::route and
redirect! return), .with(key, value) sets a route parameter,
not a flash entry - use .flash(key, value) there:
use redirect;
let _ = redirect!
.with // route param
.flash; // session flash
Cookies, headers, fragments
use ;
let _ = route
.with_cookies
.with_headers
.with_fragment // append #invoices
.without_fragment; // OR strip any prior fragment
with_fragment accepts the fragment with or without a leading #.
Calling with_fragment after without_fragment re-attaches one.
Preserve fragment across the redirect
For Inertia apps where the destination should preserve the
originating URL hash, use preserve_fragment:
use Redirect;
let _ = route.preserve_fragment;
On conversion this flashes _inertia.preserve_fragment = true into
the session; the next Inertia response reads the flag and emits
preserveFragment: true in its page object. No session scope - flag
silently dropped.
Signed redirects
Two builders wrap the URL-signing surface for one-shot redirects to named routes (password reset, email verification, download links):
use Redirect;
let r = signed_route?;
let r = temporary_signed_route?;
Both return Result<Redirect, FrameworkError> - ?-propagate the
error since Redirect converts to a Response cleanly. See
URLs for the signing surface.
Storing the intended URL
Redirect::set_intended_url writes the session's intended target
without performing a redirect - typically called from auth middleware
before redirecting to /login, so a later Redirect::intended can
recover the originally-requested URL:
set_intended_url;
Aborting from a handler
Three free functions short-circuit a handler at a given status. They
return Result<(), FrameworkError>; combine with ?:
use ;
pub async
The underlying error is FrameworkError::Domain { message, status_code },
so it renders through the same JSON envelope and 5xx sanitisation rules
as every other error path. Out-of-range status codes are coerced to
500 by the response renderer. See Error Model for
the full conversion contract.
Returning errors directly
Because Response is Result<HttpResponse, HttpResponse>, you can
return an Err arm directly - useful when the response shape is
already a specific JSON body and you want it on the wire as-is:
use ;
use json;
pub async
For anything richer - typed domain errors, validation, observability -
use the Error Model surface (AppError,
FrameworkError, #[domain_error]).
Quick reference
| Need | Use |
|---|---|
| JSON response | HttpResponse::json(v) or json_response!({...}) |
| Text response | HttpResponse::text(s) or text_response!(s) |
| HTML response | HttpResponse::html(s) |
| Raw bytes + content-type | HttpResponse::bytes_body(b, "image/png") |
| Server-Sent Events | HttpResponse::sse(stream) - see SSE |
| Chunked stream | HttpResponse::stream_bytes(stream) |
| Set status | .status(code) |
| Add header | .header(k, v) / .with_headers([...]) |
| Remove header | .without_header(name) |
| Attach cookie | .cookie(c) / .with_cookies([...]) |
| Forget cookie | .without_cookie(name) |
| Simple redirect | Redirect::to(path).into() or redirect_to(path).into() |
| Named-route redirect | redirect!("name").into() or Redirect::route("name") |
| Back redirect | Redirect::back(fallback) |
| Intended redirect | Redirect::intended(default) |
| Guest redirect (stash intended) | Redirect::guest(&req, login) |
| Set intended target | Redirect::set_intended_url(url) |
| External URL | Redirect::away(url) |
| Refresh current page | Redirect::refresh() / Redirect::refresh_for(&req) |
| Signed-route redirect | Redirect::signed_route(name, &[(k, v)])? |
| Route param on redirect | .with("key", "value") |
| Query param on redirect | .query("key", "value") |
| Flash data | .with(key, value) (or .flash on RedirectRouteBuilder) |
| Flash input | .with_input([(k, v), ...]) |
| Flash errors | .with_errors([(k, msg), ...]) |
| Named error bag | .with_errors_bag(bag, [(k, msg)]) |
| Append fragment | .with_fragment("section") |
| Strip fragment | .without_fragment() |
| Preserve fragment (Inertia) | .preserve_fragment() |
| Permanent redirect | .permanent() (301) |
| Custom redirect status | .status(303) |
| Abort early | abort_with(code, msg)?, abort_if(cond, code, msg)?, abort_unless(cond, code, msg)? |
Next
- Error Model -
FrameworkError,AppError,HttpError, and the single conversion that renders every error to anHttpResponse - Error Handling - practical handler patterns for
?,AppError, and custom domain errors - Server-Sent Events - building and consuming
sse(...)responses - URLs - signed URLs, named-route resolution, the
surface behind
Redirect::signed_route - Session - flash data, intended URLs, the bag
Redirect::with/with_input/with_errorswrites into
