werk365/etagconditionals
werk365/etagconditionals adds Laravel middleware for HTTP conditional requests, generating ETags and honoring If-None-Match and If-Match headers to return 304 Not Modified or 412 Precondition Failed responses automatically.
composer require werk365/etagconditionals. The service provider is auto-discovered in Laravel; no manual registration in config/app.php is needed on modern versions.php artisan vendor:publish --tag=etagconditionals if a config tag exists). The config is where you confirm the middleware alias name, whether it's globally registered, and any tag-generation strategy.Route::get('/articles/{article}', [ArticleController::class, 'show'])->middleware('etag');, or add it to the web group in app/Http/Kernel.php if every response should be revalidatable.GET /api/articles/{article} endpoint, produce an ETag for the representation (the package will emit one, or you set it explicitly with Laravel's native $response->setEtag(md5($payload))), and a client sending If-None-Match: "<same-tag>" gets a bare 304 Not Modified instead of the full body.PUT/PATCH/DELETE, the client sends If-Match: "<tag-it-last-saw>". If the resource changed in between, the package responds 412 Precondition Failed and the controller never runs. This is the pattern that saves you from lost updates without a version column.If-None-Match on GET/HEAD means "give me 304 if unchanged"; on PUT/POST/PATCH/DELETE it means "fail with 412 if it already exists". If-Match always means "fail with 412 if it doesn't match". Wire the middleware per-route so each verb gets the intended behaviour rather than one blanket global rule.md5(json_encode($resource)) over the serialized payload is the cheapest correct option; sha1 if you want to avoid the theoretical collision discussion in code review. Hashing $model->updated_at or $model->id is a common shortcut and a common bug — it produces the same tag for two different serializations (different with includes, different auth-scoped fields).Cache-Control. An ETag without a cacheable Cache-Control (e.g. no-cache, private — which still permits revalidation, unlike no-store) buys you nothing, because clients won't store the representation to compare against. This is the single most common integration mistake.If-None-Match from user A's tag yields a false 304. Either incorporate the auth scope into the hash or let the tag fall out of the already-scoped payload.$this->getJson($url, ['If-None-Match' => $etag])->assertStatus(304) and $this->putJson($url, [], ['If-Match' => $staleTag])->assertStatus(412). Capture the tag from an initial getJson() response header rather than hardcoding it, so the test survives serializer changes.If-Match cleanly maps to a 412 without any controller-side checks.Response::setEtag() / Response::isNotModified().W/ are weak. RFC 7232 requires strong comparison for If-Match, so a weak tag will (correctly) never satisfy an If-Match precondition. If writes always return 412, check your tag generator isn't emitting W/"...".If-Match: * means "succeed if the resource exists at all". Don't let a naive string comparison reject it.If-None-Match: "abc", "def". A middleware that only parses a single tag will misbehave when a client sends a list.Vary is your responsibility. If the same URL yields different representations by Accept, Accept-Language, or Accept-Encoding, the ETag must vary with them — and so should the Vary header. Otherwise a shared cache serves the wrong tag to the wrong client, and you get spurious 304s.curl -i -H 'Accept: application/json' https://app.test/api/thing to get the tag and headers, then repeat with -H 'If-None-Match: "<tag>"'. Confirm the ETag is byte-identical across the two responses (a missing quote or a trailing newline in the hash is enough to break it). Watch out for proxies stripping ETag or 304 bodies in dev.no-store kills revalidation. Check that a global middleware isn't overriding your Cache-Control.composer.json against your Laravel version before committing; if it doesn't declare support, fork it or inline the middleware rather than fighting the dependency resolver.Response::macro('etagFrom', ...)) over editing vendor code for tag generation.How can I help you explore Laravel packages today?