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:
WebRequestis a real PSR-7ServerRequestInterface(it composes Nyholm’sServerRequest), so anything that speaks PSR-7 works with it directly. On top of that it adds Quiote’s parameter model and convenience getters.WebResponseis the framework’s response object, which can carry and mirror a PSR-7 response.
How a request and response are built
Section titled “How a request and response are built”You rarely construct either object yourself — the framework builds them around your action:
WebRequestis built once per request by the kernel’sbuildRequestFromGlobals(): it uses Nyholm’sServerRequestCreator::fromGlobals(), applies reverse-proxy corrections (X-Forwarded-*, see below), then wraps the result in the class named by yourrequestfactory role (WebRequestby default). Its parameters come from three places: the query string, the parsed body (PayloadParsingMiddlewaredecodes JSON and form bodies), and route placeholders (promoted byValidationMiddleware).WebResponseis created from yourresponsefactory role. Your view and middlewares write to it as the request travels the pipeline; at the end,DispatchMiddlewarebridges it into the final Nyholm PSR-7 response — applying the output type’s headers and addingX-Content-Type-Options: nosniff— whichHttpEmittersends to the client.
Kernel builds the
WebRequestfrom globals → the pipeline runs → your action reads input and writes to theWebResponse→DispatchMiddlewarebridges it to a PSR-7 response →HttpEmitteremits it.
See The request lifecycle for the complete path.
Reading request input
Section titled “Reading request input”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}Strict parameter access
Section titled “Strict parameter access”Parameter access is validated by default. Asking for a parameter that no validator whitelisted throws UnvalidatedParameterAccessException — unless 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 throwingThis 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:
| Method | Returns |
|---|---|
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. |
JSON bodies
Section titled “JSON bodies”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).
Cookies, headers, files
Section titled “Cookies, headers, files”$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 filesURL and method
Section titled “URL and method”Beyond the PSR-7 getMethod() / getUri(), WebRequest offers direct getters: getUrlScheme(), getUrlHost(), getUrlPort(), getUrlPath(), getUrlQuery(), getUrl(), and isHttps().
Mutating a request
Section titled “Mutating a request”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:
| Method | Effect |
|---|---|
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);URL metadata
Section titled “URL metadata”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:
| Deprecated | Preferred |
|---|---|
$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:
| Where | Sync 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.
Shaping the response
Section titled “Shaping the response”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();Body, status, headers
Section titled “Body, status, headers”$response->setContent($body);$response->setContentType('application/pdf');$response->setHttpStatusCode(201);$response->setHttpHeader('Cache-Control', 'no-store');$response->addHttpHeader('Vary', 'Accept'); // append rather than replacesetHttpStatusCode() 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.
Cookies
Section titled “Cookies”$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 /.
Redirects
Section titled “Redirects”$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.
The PSR-7 view of the response
Section titled “The PSR-7 view of the response”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.
Forwarded headers and trusted hosts
Section titled “Forwarded headers and trusted hosts”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(andX-Original-Host), or- the RFC 7239
Forwardedheader 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:
return [ 'core.trusted_hosts' => [ 'example.com', 'www.example.com', '/^([a-z0-9-]+\.)?example\.com$/i', ],];core.trusted_hosts: - example.com - www.example.com - '/^([a-z0-9-]+\.)?example\.com$/i'<!-- Config/settings.xml — inside <settings> --><setting name="core.trusted_hosts"> <value>example.com</value> <value>www.example.com</value> <value>/^([a-z0-9-]+\.)?example\.com$/i</value></setting>Leaving core.trusted_hosts empty (the default) applies no restriction. Set it in production once your host names are known.
Worker-mode note
Section titled “Worker-mode note”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.