nette/http
Nette HTTP is a lightweight PHP library for handling HTTP requests and responses. It provides clean APIs for headers, cookies, sessions, URL parsing, file uploads, and response output, making it easy to build robust web applications and services.
nette/http is a server-side HTTP abstraction, not a framework and not a client. Four objects do almost all the work: Request, Response, Session, and the URL value objects (Url, UrlImmutable, UrlScript). Everything else — RequestFactory, FileUpload, IPAddress, UrlValidator, the DI extensions — is either plumbing that constructs those objects or a helper you reach for on specific problems.
composer require nette/http
PHP 8.3+ is required as of v3.4.0. The package is usable standalone, but the DI extensions (HttpExtension, SessionExtension) only make sense inside a Nette DI container; if you are not on Nette, you instantiate by hand and skip the config layer entirely.
Where to look first: src/Http/RequestFactory.php and src/Http/Request.php. RequestFactory is the seam between PHP globals and the rest of the library — it is where proxy trust, HTTPS forcing, host/port detection and CLI base URLs are decided. Once you understand the factory, Request is just a read-only facade and the rest follows. src/Http/Response.php and src/Http/Session.php are the next two files. The API reference at api.nette.org is generated and reliable; there is no narrative manual worth reading end-to-end, and the release notes in release/v3.*.md are effectively the changelog-of-record for behavioural subtleties.
First use case — a controller/endpoint that reads input, writes a hardening header and a cookie:
use Nette\Http\IRequest;
use Nette\Http\IResponse;
use Nette\Http\SameSite;
final class PingController
{
public function __construct(
private IRequest $httpRequest,
private IResponse $httpResponse,
) {}
public function __invoke(): void
{
$ua = $this->httpRequest->getHeader('User-Agent') ?? 'unknown';
$id = $this->httpRequest->getQuery('id'); // sanitized, null if absent
$this->httpResponse->setHeader('X-Request-Source', 'ping');
$this->httpResponse->setCookie(
name: 'ping',
value: $ua,
expires: null, // session cookie
path: '/',
sameSite: SameSite::Lax,
);
$this->httpResponse->setCode(IResponse::S200_OK);
}
}
Type-hint IRequest / IResponse, not the concrete classes — the DI extension binds to the interfaces, and this is also what makes the objects trivially fakeable in tests.
For CLI entry points (cron, queue workers, MCP servers), configure http: baseUrl: https://app.example.com or call RequestFactory::setBaseUrl(). That is what makes %baseUrl%, link generation, template $baseUrl and asset URLs work outside a web request; it kicks in only when no host can be detected, so it cannot corrupt web traffic.
Treat RequestFactory as the composition root. Everything that is environment-dependent — trusted proxies, forced scheme, base URL, URL filters — is configured there, once, at bootstrap:
$factory = new Nette\Http\RequestFactory;
$factory->setProxy(
secret: '10.0.0.0/8', // trusted proxy range(s)
forwarded: false, // ignore RFC 7239 Forwarded
xForwarded: true, // trust X-Forwarded-For/Proto/Host/Port
);
$factory->setForceHttps(true); // TLS terminated at the LB
$factory->setBaseUrl('https://app.example.com');
$httpRequest = $factory->fromGlobals();
$httpResponse = new Nette\Http\Response;
$session = new Nette\Http\Session($httpRequest, $httpResponse);
Doing this in one place, rather than sprinkling $_SERVER reads through the codebase, is the single highest-leverage habit with this package. The DI equivalent is the http: / session: config sections, which is the normal path in a Nette application.
Proxy configuration must match reality, not hope. v3.4.1 changed the default to trust only X-Forwarded-* from a trusted proxy. If your ingress (nginx, Traefik, Cloudflare, AWS ALB) emits Forwarded, you must set http: proxyHeaders: forwarded (or both); if it emits X-Forwarded-* and you have no proxy at all, trusting nothing (none) is correct and prevents a client-supplied header from spoofing scheme or host. Pick deliberately per environment.
Cookies: lean on the modern attributes instead of hand-rolling headers. SameSite is a real enum now, Partitioned (CHIPS) is supported for third-party contexts, Max-Age is emitted so client clock skew is irrelevant, and Secure is forced automatically when SameSite=None — the browser-rejection trap is closed for you:
$response->setCookie(
'session_id', $sid,
expires: '20 minutes', // relative-ish; parsed by Helpers::expirationToSeconds()
path: '/',
domain: '', // explicit host-only ('' ≠ null, see Gotchas)
secure: true,
httpOnly: true,
sameSite: SameSite::None,
partitioned: true,
);
Session configuration belongs in config, and so does the handler. Use session: handler: to inject a SessionHandlerInterface (Redis, DB) rather than calling session_set_save_handler() yourself — the extension validates option names and refuses to let security options be bypassed. For stateless-ish endpoints (JSON APIs, webhooks) where you only need to read a flash message or CSRF token, enable readAndClose so the session lock is released immediately; that alone removes a common source of request serialization under load.
Same-site and origin checks: use isFrom(), not isSameSite(). Request::isFrom() builds on Sec-Fetch-Site/Dest/User and transparently falls back to a strict cookie on Safari < 16.4. Request::getOrigin() similarly follows RFC 6454 strictly and returns null for anything that is not a bare scheme://host[:port] — which is what you want for CORS/CSRF decisions, because silently accepting a path-bearing Origin is a vulnerability, not a convenience.
URLs are value objects — build them, do not concatenate. UrlImmutable and UrlScript give you resolve(), withoutUserInfo(), isAbsolute(), removeDotSegments(), canonicalisation and query parsing that handles spaces, dots and the ; separator. In a Nette app this is what LinkGenerator sits on; standalone, use UrlScript::getRelativePath() when you need to map the current path to a router.
File uploads: the API is deliberately paranoid, but it is not a validator.
if ($file->hasFile() && $file->isOk()) {
if (!$file->isImage() || !in_array($file->getContentType(), Nette\Http\FileUpload::IMAGE_MIME_TYPES, true)) {
throw new InvalidArgumentException('Unsupported upload');
}
$file->move(__DIR__ . '/../storage/' . $file->getSanitizedName());
}
getSanitizedName() and getImageFileExtension() are convenience, not a security boundary — the content type still comes from the client unless you re-sniff it server-side.
Outbound HTTP is where the SSRF kit earns its place. Any feature that fetches a user-supplied URL (webhooks, avatars, importers, RSS) should route through UrlValidator with an allowlist, and then — critically — pin the validated addresses so DNS cannot rebind between check and request:
$validator = new Nette\Http\UrlValidator(
allowedSchemes: ['https'],
allowedPorts: [443],
blockPrivateRanges: true,
resolveDns: true,
);
$url = 'https://user-supplied.example/hook';
$ips = $validator->validate($url); // throws on failure
$ch = curl_init((string) $url);
curl_setopt($ch, CURLOPT_RESOLVE, [
parse_url($url, PHP_URL_HOST) . ':443:' . $ips[0],
]);
Pair with IPAddress for the predicates (isPublic(), isLoopback(), isLinkLocal(), isInRange(), IPv4-mapped IPv6 normalisation) when you need to reason about an address outside the validator.
Bridge to PSR-7 at the edge, not throughout. There is no PSR-7/PSR-15 adapter in this package. For third-party libraries that demand PSR-7 (OAuth clients, mailers), write a thin conversion in each direction and keep nette/http as the only HTTP representation inside your domain. Do not let two request abstractions leak into controllers.
Testing. Because RequestFactory and the value objects are constructible from arrays, you can build a Request from a hand-written server array and a Response/Session against it, with no web server. That is the pattern for unit-testing anything that touches headers, cookies or proxy resolution — including the multi-hop Forwarded chain cases, which are otherwise very hard to verify on a real proxy.
v3.4.1 is a real behaviour change, not a patch. Two things will bite on upgrade: (1) X-Forwarded-* is now the only header trusted by default, so a proxy emitting Forwarded will see wrong scheme/host/address until you set proxyHeaders; (2) getRemoteAddress() returns null instead of a non-IP string when the innermost forwarded value is not a valid IP. Any code doing strlen($request->getRemoteAddress()) or storing it in a non-nullable column will fail. Audit those call sites before upgrading.
setCookie() distinguishes null from ''. null means "not specified, use defaults"; '' means "explicitly empty" — host-only cookie, or / for path. This is a silent-breakage vector if you pass variables that might be empty strings, e.g. a $domain read from config that defaults to '' will now yield a host-only cookie rather than the configured default domain. Be explicit about which of the two you mean.
SameSite=None forces Secure, which breaks local development over plain HTTP. The cookie is simply not stored by the browser, and the symptom is "sessions randomly don't persist" on http://localhost. Either serve local dev over TLS or use a conditional configuration that drops to Lax outside production. Related: cookieSecure: auto is the default in the DI extension and derives from the detected scheme — behind a proxy that does not advertise TLS, that resolves to false unless forceHttps or correct proxy headers are configured.
Session cookie parameters are inherited from Response. cookie_path, cookie_domain and cookie_secure default to the response's values, and session cookie parameters cannot be changed once the session is active. If you are mixing manual session_start() with Nette's Session, the session_status() check means Nette will not reconfigure an already-started session — order your bootstrap so Session is configured first, or accept the host's php.ini values.
getRemoteHost() is deprecated and returns null. Reverse DNS was slow and a privacy liability; if you genuinely need a hostname, resolve it yourself from getRemoteAddress() and cache it. Do not build authorisation on it.
disableNetteCookie is not free. The nette-samesite cookie is how Request::isFrom() does its job on Safari < 16.4; Helpers::initCookie() re-sends it on each request. Turning the cookie off gains you a slightly cleaner cookie jar and loses reliable same-site detection on older Safari. Decide which matters more before flipping it.
Url::$user / $password are deprecated. Do not parse or construct credentials into URL objects; treat userinfo in a URL as hostile input. UrlImmutable::withoutUserInfo() exists precisely for stripping it before logging or redirecting.
build() was removed from UrlImmutable/UrlScript in favour of resolve(). Old snippets floating around the web still call build(); they will fatal. Also note Url::canonicalize() no longer percent-encodes ", so byte-level assertions on canonicalised URLs in tests need updating.
UrlValidator is only as good as the pinning. DNS-based range checks are classic TOCTOU: validating a hostname and then letting cURL re-resolve it defeats the whole exercise. Always convert the validated result into a CURLOPT_RESOLVE (or equivalent) pin, and reject redirects that leave the original host unless you re-validate each hop.
Integer 0 as cookie expiration is deprecated. Use null for a session cookie. The unified parser (Helpers::expirationToSeconds()) also treats numeric values as relative seconds while DateTimeInterface and textual strings like '20 minutes' or '2024-01-01' resolve as absolute — a subtle asymmetry that will surprise you if you pass a Unix timestamp and get a cookie valid until 1970-ish arithmetic.
Subclassing is constrained. Several classes and members are final (a deliberate v3.0 decision). Extend through RequestFactory, the SessionHandlerInterface handler option, or by decorating the IRequest/IResponse interfaces — not by inheriting from Response or Url.
Debugging: in a Nette app, the Tracy bar's request and session panels show detected URL, headers, cookie parameters and session state, which is usually faster than dumping $_SERVER. If a generated link is wrong, check the factory's detected host/scheme first, before suspecting the router.
Overall assessment. This is unglamorous, well-maintained plumbing with a narrow scope and unusually careful handling of the parts people get wrong (proxy chains, cookie attributes, same-site detection, SSRF). Its real leverage is concentrated in RequestFactory configuration, cookie/session hardening, and UrlValidator — if your application has reverse proxies, cookie-sensitive sessions, or any user-supplied-URL fetching, those three areas repay adoption almost immediately. If you are outside the Nette ecosystem, its value drops sharply: no PSR-7/PSR-15 interop, no middleware, no HTTP client, and a DI extension you cannot use — so you would be adopting it as three or four standalone value objects, which is genuinely useful but a much smaller win.
How can I help you explore Laravel packages today?