Start learning free

LESSON 126 OF 173 · BACKEND: SPEED

ETags and 304

Practice

The cache saves the server's work, but the shop still sends the whole product list every time, even to a browser that got the very same list a minute ago. An ETag fixes that: a short fingerprint of an answer, sent in a header next to it. The same content always gives the same ETag; change one price and it is completely different.

A browser that has a copy sends its ETag back in the If-None-Match header, meaning "send it only if it isn't this one". If the fingerprint still matches, the server answers 304 Not Modified with an empty body, and the browser shows its own copy. A few bytes travel instead of the whole list.

One place for every route: Spring runs every @Component class that extends OncePerRequestFilter around every request. Its doFilterInternal method gets the request and the response, and chain.doFilter(request, response) runs the route. For example, this one adds a header to every answer:

@Component
class ShopNameFilter extends OncePerRequestFilter {
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        response.setHeader("X-Shop", "AlgoShop");
        chain.doFilter(request, response);
    }
}

To hash an answer, the filter needs its body after the route has run. A route normally writes its body straight out, so wrap the response in a ContentCachingResponseWrapper: it keeps the body in memory until you call copyBodyToResponse().

Your code must give these answers, in this order. The 304s save the trip, not the query: the cache still does that job, so the count stays at 4.

GET /products  →  200  ETag: "d62f8998b9c6175bf98ed96c8fa6de98"  [all 8 products, Mug at 12]
GET /products  If-None-Match: "d62f8998b9c6175bf98ed96c8fa6de98"  →  304  ETag: "d62f8998b9c6175bf98ed96c8fa6de98"  (empty body)
PATCH /products/1  {"price": 13}  →  200  {"id": 1, "price": 13}  (no ETag)
GET /products  If-None-Match: "d62f8998b9c6175bf98ed96c8fa6de98"  →  200  ETag: "426305b11dfff0b0d6b5151683164898"  [all 8 products, Mug at 13]
GET /products  If-None-Match: "426305b11dfff0b0d6b5151683164898"  →  304  ETag: "426305b11dfff0b0d6b5151683164898"  (empty body)
GET /products/99  →  404  {"error": "Product not found"}  (no ETag)
GET /debug/queries  →  200  {"queries": 4}

Your task: below ShopApplication, add a @Component class EtagFilter that extends OncePerRequestFilter. In doFilterInternal, wrap the response, run the route, and for a GET that answered 200, hash the kept body with DigestUtils.md5DigestAsHex and send it, in double quotes, as the ETag header. If the browser's If-None-Match holds that same ETag, answer 304 with an empty body. Last, send the kept body.


Assignment
Follow the Coding Tutorial to add ETags and 304 answers to the shop's successful GET requests.


Hint
wrapper.getContentAsByteArray() gives the body's bytes. An ETag is written in double quotes, so put "\"" on both sides of the hash. Compare it with request.getHeader("If-None-Match"), then use wrapper.setStatus(304) and wrapper.resetBuffer(). Without wrapper.copyBodyToResponse() at the end, every answer goes out empty.


STEP 1 OF 11
    Ctrl + Enter to check