Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Rest E Tag Bundle Laravel Package

kleijnweb/rest-e-tag-bundle

Symfony bundle that adds ETag and conditional-request support to REST APIs. It generates entity tags, honors If-None-Match and If-Match headers, and returns 304 Not Modified responses to improve caching and reduce bandwidth for JSON endpoints.

View on GitHub
Deep Wiki
Context7

Getting Started

This is the first stable (v1.0.0) release, so treat it as "feature complete" but still verify behaviour against your own HTTP edge cases before rolling it out broadly.

What it is. kleijnweb/rest-e-tag-bundle provides server-side ETag handling for REST resources: it generates ETags for representations, stores/retrieves them per resource, and enforces the conditional request contract (If-Match → 412 Precondition Failed, If-None-Match → 304 Not Modified, plus If-Match: *). The "bundle" part is the Symfony integration; the underlying library is where the actual logic lives.

The framework caveat, up front. Despite the "Laravel/PHP package" framing, this is Symfony-first. Do not expect a service provider. In Laravel you will consume the core library and write your own middleware — budget half a day for that, and skim the bundle's listener class as the reference implementation of the correct ordering.

Minimal steps.

  1. composer require kleijnweb/rest-e-tag-bundle (or just the core kleijnweb/rest-e-tag if you only want the primitives).
  2. Read, in this order: the Etag value object (note the weak/strong distinction), the EtagManager/EtagValidator entry points, then the EtagStorageInterface implementations. The bundle's kernel-event listener is your ordering reference.
  3. Verify the release artifact: the published MD5 is ae11325c6560442e146dfcec40544ef6. Check it against the downloaded tag/archive before you commit the version to composer.json.
  4. First use case: a single GET /api/orders/{id} endpoint. Generate an ETag from the response body, return it in the ETag header, and honour If-None-Match with a 304 and empty body. Do this one endpoint end-to-end before touching If-Match/writes — the read path and the write path have different failure modes and mixing them in one commit makes debugging miserable.

Implementation Patterns

Middleware for the read path. Register a Terminate/after middleware that hashes the outgoing representation and sets ETag. Compute it from the final serialised bytes, not from the model — anything that varies the payload (sparse fieldsets, includes, locale, current-user scoping) must vary the ETag, or you will serve stale-looking caches. Hash the body plus any Vary dimensions you can't express in the URL.

If-None-Match before you render. Better than the after-middleware: resolve the resource, compute the ETag cheaply (a version column or updated_at on the model is usually enough and avoids full serialisation), compare against If-None-Match, and short-circuit to 304 before doing expensive serialisation. Fall back to body-hash ETags on endpoints where no version column exists.

Optimistic concurrency on writes. For PUT/PATCH/DELETE, require If-Match (or accept * for unconditional overwrite where that's genuinely safe) and return 412 on mismatch. The pattern that works day-to-day:

  1. Client GETs the resource, receives the ETag.
  2. Client sends the mutation with If-Match: <that etag>.
  3. Server re-reads the stored ETag inside the same transaction as the write, compares, and either applies or returns 412 with the current representation so the client can re-diff.

Do the compare-and-write atomically. Wrapping it in DB::transaction with a row lock (lockForUpdate) is the pragmatic answer; a read-check-then-write outside a transaction gives you a window where two clients both pass validation.

Storage. The storage abstraction is the integration point you'll actually touch. Options in order of preference: a version/updated_at column on the model (cheap, transactional, no extra table), Redis keyed by resource URI (fast, but you own invalidation), or a dedicated ETag table. Avoid recomputing from the database on every conditional request if you can help it — the point of 304 is to be cheap.

Wire it as opt-in per route. Apply the middleware to a route group rather than globally. Collection endpoints are the trap: pagination, filtering and sort order are all part of the representation, so either include them in the ETag input or leave collections out of conditional handling entirely until you've thought it through.

Response shape. Always send ETag on 200 for a resource that supports conditionals, and send Cache-Control with it (private, no-cache for authenticated per-user resources — no-cache still permits revalidation via 304, which is exactly what you want). Send Vary: Authorization, Accept on anything user- or format-scoped.

Testing. Feature-test both branches explicitly: same ETag returns 304 with empty body; stale If-Match returns 412; missing If-Match on a protected write returns 428 Precondition Required if you choose to enforce it (the library gives you the validation primitives, the policy is yours). Also test that an unmodified resource re-fetched twice is byte-identical — non-deterministic JSON key order or timestamps in the payload will silently defeat the whole scheme.

Gotchas and Tips

The framework mismatch is the main cost. No Laravel service provider, no published config. You are porting a Symfony listener into middleware. Everything in the bundle's DI wiring has to be rebuilt by hand or replaced with a small container binding — keep it thin and don't emulate the bundle's config tree.

Weak vs strong ETags. Know which one you're emitting. W/"..." is correct when the bytes may differ but the semantics are equivalent (e.g. gzip, header-only differences). A strong ETag that you then vary via Content-Encoding or Accept is a lie, and If-Match with strong comparison semantics will start producing spurious 412s behind a compressing proxy/CDN. If you're unsure, prefer deriving from a version column and emitting strong ETags without content negotiation.

Proxies and CDNs will rewrite or drop ETag. Confirm the header survives your ingress. Many CDNs strip ETag or transform it on compress; verify with curl -I against the deployed host, not just artisan serve.

If-None-Match and If-Match are not symmetrical. If-None-Match uses weak comparison (so W/"x" matches "x"), If-Match uses strong comparison. If your test suite asserts "same tag ⇒ same outcome" for both headers, it will pass locally and fail in production. Also: If-None-Match takes precedence over If-Modified-Since; don't implement both and expect sane results.

412 responses need a body. Return the current resource representation (with its current ETag) so the client can merge without a second round trip. An empty 412 forces a GET and doubles your write latency under contention.

Don't ETag your error responses, and don't let the middleware run on exception paths — a cached 500 is a bad day.

Invalidation is on you. If you store ETags separately from the resource, every write path must update or delete the stored tag. Miss one (a queued job, an admin console, a direct DB edit) and clients will receive 304s for content that has changed. This is the single most common source of "the API is stale" bug reports. Prefer deriving the tag from the row so it can't drift.

Release verification. v1.0.0 publishes an MD5 (ae11325c6560442e146dfcec40544ef6) rather than a signed tag — MD5 is fine as a transport-integrity check but proves nothing about authorship. Pin the exact version, commit composer.lock, and treat future releases the same way. If you need stronger guarantees, verify the tag signature on GitHub directly.

Extension points. The storage interface and the ETag value object are the two places worth subclassing/wrapping. Everything else (the generator, the validator) is small enough that forking the ~few-hundred-line core into app/Support/ is a legitimate option if you need behaviour the library doesn't cover — you'll spend less time than fighting the abstractions.

Version risk. This is 1.0.0 with no prior public stability record. Keep your integration behind a single middleware class and a single storage binding so that a 2.0 with a changed contract is a one-file change, and add a test that pins the exact 304/412 semantics you depend on.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console