Skip to content

Requests and responses

Every action receives a WebRequest — the incoming request — and, through the context, can shape a WebResponse — the outgoing response. This page is the API reference for both: reading input, mutating the request, and setting headers, cookies, status codes, and redirects.

Two facts worth knowing up front:

  • WebRequest is a real PSR-7 ServerRequestInterface (it composes Nyholm’s ServerRequest), so anything that speaks PSR-7 works with it directly. On top of that it adds Quiote’s parameter model and convenience getters.
  • WebResponse is the framework’s response object, which can carry and mirror a PSR-7 response.

You rarely construct either object yourself — the framework builds them around your action:

  • WebRequest is built once per request by the kernel’s buildRequestFromGlobals(): it uses Nyholm’s ServerRequestCreator::fromGlobals(), applies reverse-proxy corrections (X-Forwarded-*, see below), then wraps the result in the class named by your request factory role (WebRequest by default). Its parameters come from three places: the query string, the parsed body (PayloadParsingMiddleware decodes JSON and form bodies), and route placeholders (promoted by ValidationMiddleware).
  • WebResponse is created from your response factory role. Your view and middlewares write to it as the request travels the pipeline; at the end, DispatchMiddleware bridges it into the final Nyholm PSR-7 response — applying the output type’s headers and adding X-Content-Type-Options: nosniff — which HttpEmitter sends to the client.

Kernel builds the WebRequest from globals → the pipeline runs → your action reads input and writes to the WebResponseDispatchMiddleware bridges it to a PSR-7 response → HttpEmitter emits it.

See The request lifecycle for the complete path.

The primary way to read input is getParameter(), which merges query and body parameters:

public function executeWrite(WebRequest $rd)
{
$name = $rd->getParameter('name');
$page = $rd->getParameter('page', 1); // default if absent
$tags = $rd->getParameter('tags'); // array params supported
}

Parameter access is validated by default. Asking for a parameter that no validator whitelisted throws UnvalidatedParameterAccessExceptionunless you pass a default, in which case the default is returned:

$rd->getParameter('email'); // throws if 'email' was never validated
$rd->getParameter('email', null); // returns null instead of throwing

This is a safety feature, not an obstacle: it means a typo’d or unvalidated field fails loudly instead of silently feeding unchecked input into your action. Declare fields with a validator (see Input validation) and they become readable; the validator can also export a sanitized value that getParameter() then returns. To read a value you deliberately did not validate, pass a default.

Related reads:

MethodReturns
hasParameter($name)Whether the parameter is present and whitelisted — false for an unvalidated key, not an exception.
getParameters($source) / getAll($source)All params from a source: parameters, cookies, files, headers, attributes, runtime.
getParameter($name, $default)A single value, with strict-access behaviour above.

For a request with Content-Type: application/json (typically a write/update), PayloadParsingMiddleware decodes the JSON body and mirrors its fields into the parameters automatically, so getParameter('field') reads a JSON field the same way it reads a form field. An empty or malformed JSON body fails the request with a 400 response rather than silently yielding nothing (this strict parsing is the default; it can be relaxed with QUIOTE_JSON_STRICT=0).

$rd->getCookieParams(); // all cookies
$rd->hasCookie('session');
$rd->getHeaderLine('Accept'); // PSR-7 header access
$rd->hasHeader('Authorization');
$rd->getUploadedFile('avatar'); // first UploadedFileInterface, or null
$rd->getUploadedFileArray('photos'); // flat list of uploaded files

Beyond the PSR-7 getMethod() / getUri(), WebRequest offers direct getters: getUrlScheme(), getUrlHost(), getUrlPort(), getUrlPath(), getUrlQuery(), getUrl(), and isHttps().

WebRequest’s write API mirrors its read API — a runtime parameter store on top of PSR-7’s own with*() methods — but every one of these returns a new WebRequest rather than changing the object you called it on:

MethodEffect
setParameter($name, $value)Set a runtime parameter, auto-whitelisted for strict access.
appendParameter($name, $value)Append a value to a list-style runtime parameter.
removeParameter($name, $source)Remove a parameter from the runtime store (or parameters for query/body).
declareParameter($name) / declareParameters($names)Whitelist parameter name(s) for strict access without setting a value.
enforceValidatedParameters($keys)Merge names into the strict-access whitelist.
clearParameters()Drop query, body, and runtime parameters.
setAttribute($name, $value)Set a PSR-7 request attribute.
appendAttribute($name, $value)Append a value to a list-style attribute.

The most common mistake — setting a runtime parameter and expecting the current $request variable to reflect it:

// WRONG — return value discarded; $request is unchanged
$request->setParameter('total', $total);
// RIGHT — capture the new instance
$request = $request->setParameter('total', $total);

Seven older setters write the request’s URL metadata: setUrlScheme(), setUrlHost(), setUrlPort(), setRequestUri(), setUrlPath(), setUrlQuery() and setProtocol(). All seven still work, keep their void signature, and now also rewrite the wrapped PSR-7 URI — previously they didn’t, so after setUrlHost('other.test') a check reading getUrlHost() and one reading getUri()->getHost() disagreed, and a host- or scheme-based guard passed or failed depending on which the caller happened to read.

They’re deprecated in favour of with*() counterparts that return a new instance, consistent with the rest of the write API:

DeprecatedPreferred
$r->setUrlScheme('https')$r = $r->withUrlScheme('https')
$r->setUrlHost('h')$r = $r->withUrlHost('h')
$r->setUrlPort(8443)$r = $r->withUrlPort(8443)
$r->setRequestUri('/p?a=b')$r = $r->withRequestUri('/p?a=b')
$r->setUrlPath('/p')$r = $r->withUrlPath('/p')
$r->setUrlQuery('a=b')$r = $r->withUrlQuery('a=b')
$r->setProtocol('HTTP/1.0')$r = $r->withProtocol('HTTP/1.0')

Reassigning isn’t always enough: sync back to Context

Section titled “Reassigning isn’t always enough: sync back to Context”

Context::getRequest() is how the rest of the framework — templates, later middleware, the next pipeline stage — reaches “the current request”. Reassigning your own local $request variable doesn’t change what Context::getRequest() hands back to anyone else; if the new value needs to be visible beyond the current method, sync it explicitly:

$request = $request->setParameter('total', $total);
$this->getContext()->setRequest($request);

Whether you need that sync depends on where you’re calling from:

WhereSync needed?
Inside a PSR-15 middleware, immediately passing $request into $handler->handle($request)No — you’re already forwarding the new value.
Inside Action::validate() / validate{Method}()Yes — ValidationMiddleware re-fetches Context::getRequest() right after validate() runs.
Inside Action::execute{Method}()Yes — ActionExecutor::doExecute() re-fetches Context::getRequest() right after the action runs, before creating the view.
Inside Action::handle{Method}Error()Yes — ValidationMiddleware re-fetches Context::getRequest() right after the error handler, before creating the error view.
Inside View::execute{Method}()Only if something rendered afterward needs to read it back — there’s no further re-fetch once a view runs, so sync immediately if at all.

When in doubt, sync anyway: an unnecessary $this->getContext()->setRequest($request) is a harmless no-op, while a missing one is a parameter or attribute that silently vanishes.

Actions usually return a view name and let a view and template produce the body. When you need to set headers, cookies, a status code, or a redirect directly, reach the response through the controller — injected, like anything else in the container:

public function __construct(private readonly Controller $controller) {}
// ...
$response = $this->controller->getResponse();
$response->setContent($body);
$response->setContentType('application/pdf');
$response->setHttpStatusCode(201);
$response->setHttpHeader('Cache-Control', 'no-store');
$response->addHttpHeader('Vary', 'Accept'); // append rather than replace

setHttpStatusCode() accepts any code in 100–599 and throws on anything outside that range (Quiote\Http\HttpStatus is the authority). It deliberately doesn’t check membership of a table of known codes: a per-protocol whitelist made ordinary codes like 422, 429, 308, 451, 507 and 511 unsettable, so code composing a response through WebResponse could not emit them at all.

If you want a narrower range, declare it in a subclass — the framework never sets $httpStatusCodes itself:

class StrictResponse extends WebResponse
{
/** @var ?array<int, string> */
protected $httpStatusCodes = ['200' => 'OK', '404' => 'Not Found'];
}

$http10StatusCodes and $http11StatusCodes are deprecated and no longer consulted. They remain as protected properties for subclasses that read them.

$response->setCookie('prefs', $value, lifetime: 86400, samesite: 'Strict');
$response->unsetCookie('prefs');

setCookie() fills unspecified arguments from the response’s cookie defaults, which are secure by default: HttpOnly on, SameSite=Lax, and Secure set automatically when the request is HTTPS. Path defaults to /.

$response->setRedirect('/login'); // 302 by default
$response->setRedirect('/moved', 301);
return \Quiote\View\View::NONE; // "I produced the response myself"

When an action handles the response itself — a redirect, a streamed file — return View::NONE (null) so the framework doesn’t also try to render a view. On send, a redirect forces Content-Length: 0 and sets the Location header.

A view or action can reach a PSR-7 wrapper around the response with getPsrResponse(), which hands back a Quiote\Http\PsrResponseAdapter. It is a real immutable PSR-7 response: with*() clones and leaves both the adapter and the underlying WebResponse untouched, as ResponseInterface requires.

That means the PSR-7 idiom does nothing useful unless you keep the clone — and since the response that actually gets sent is the WebResponse, the clone is usually not what you want:

// Does nothing — the clone is discarded, and the sent response is untouched.
$this->getInitContext()->getPsrResponse()->withHeader('X-Thing', '1');
// Write to the response that gets sent.
$this->getResponse()->setHttpHeader('X-Thing', '1');

From code holding an adapter rather than a view, getLegacy() is the mutable response underneath:

$adapter->getLegacy()->setHttpHeader('X-Thing', '1');

withStatus() validates its argument and throws InvalidArgumentException as PSR-7 mandates, and getReasonPhrase() returns the real phrase.

Behind a reverse proxy or load balancer, the real client scheme, host, and port arrive in headers rather than the connection. Quiote reads them before it builds the request, so isHttps(), getUrlHost(), and cookie Secure reflect the external request:

  • X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port (and X-Original-Host), or
  • the RFC 7239 Forwarded header as a fallback.

These correct $_SERVER (scheme, host, port) up front so the rest of the framework sees consistent values.

To defend against Host-header poisoning, set core.trusted_hosts — a list of exact hostnames and/or /regex/ patterns. When set, a request whose Host matches none of them has its Host replaced with the first literal (non-regex) entry in the list — so keep at least one plain hostname there, not only patterns:

Config/settings.php
return [
'core.trusted_hosts' => [
'example.com',
'www.example.com',
'/^([a-z0-9-]+\.)?example\.com$/i',
],
];

Leaving core.trusted_hosts empty (the default) applies no restriction. Set it in production once your host names are known.

Both WebRequest and WebResponse implement the framework’s reset contract and are cleared between requests in worker mode — the response’s status, headers, cookies, and redirect all reset to a clean state, and the incoming WebRequest itself is rebuilt wholesale from the new request’s globals rather than reused, so no runtime parameter, attribute, or URL override from a previous request can carry over. As with sessions, the takeaway is to read and write through these objects rather than the superglobals, so the reset actually covers your state.