Hone logo
Hone
Problems

Implementing HTTP Response Caching in Go

Many web applications make repeated identical requests to a server. Fetching the same data over and over again can be inefficient, consuming unnecessary bandwidth and increasing server load. This challenge focuses on building a simple yet effective HTTP response caching mechanism in Go to mitigate these issues. You will implement a middleware that intercepts outgoing HTTP responses and stores them in a cache. Subsequent identical requests will then be served directly from the cache, improving performance.

Problem Description

Your task is to create an HTTP middleware in Go that intercepts responses from an upstream HTTP server and caches them. When the same request is made again, the middleware should return the cached response instead of forwarding the request to the upstream server.

Key Requirements:

  1. Request Matching: The cache should be keyed by the request method and URL.
  2. Caching Responses: Successful responses (e.g., HTTP status codes 2xx) should be stored in the cache.
  3. Cache Eviction (Basic): For simplicity, we will implement a Time-To-Live (TTL) for cached entries. Entries older than the TTL should be considered stale and a fresh request should be made to the upstream server.
  4. Middleware Integration: The caching logic should be implemented as an HTTP middleware, allowing it to be easily integrated with an existing HTTP server or client.
  5. Handling Cache Misses: If a request is not found in the cache (or is stale), it should be forwarded to the upstream server, and its response should be cached (if eligible).
  6. Handling Cache Hits: If a valid cached response is found, it should be returned directly to the client.

Expected Behavior:

  • The first request for a specific URL and method will be sent to the upstream server. The response will be stored in the cache.
  • Subsequent identical requests (within the TTL) will immediately return the cached response.
  • If a cached response expires (exceeds TTL), the next identical request will again hit the upstream server, and the response will be re-cached.
  • Non-successful responses (e.g., 4xx, 5xx status codes) should generally not be cached, or at least not with the same TTL as successful responses. For this challenge, assume only 2xx responses are cacheable.

Edge Cases to Consider:

  • Concurrent Requests: How will multiple identical requests arriving simultaneously be handled? For this challenge, you can assume a simple in-memory cache with basic locking.
  • Large Responses: How to handle potentially large response bodies? For this challenge, you can assume response bodies are manageable and can fit into memory.
  • Request Headers/Body: For this challenge, focus on caching based on method and URL only. Do not consider request headers or request bodies for cache keying.

Examples

Example 1: First Request and Cache Population

  • Input (simulated):
    • A GET request to http://example.com/data is made.
    • The upstream server responds with:
      • Status: 200 OK
      • Body: {"message": "This is some data."}
      • Headers: Content-Type: application/json
  • Output:
    • The client receives the 200 OK response with the JSON body.
  • Explanation:
    • This is the first request, so it's a cache miss. The request is forwarded to the upstream.
    • The response is valid (200 OK) and will be stored in the cache with a key derived from GET and http://example.com/data, along with the response body and headers.

Example 2: Subsequent Request (Cache Hit)

  • Input (simulated):
    • Another GET request to http://example.com/data is made shortly after Example 1, within the TTL.
  • Output:
    • The client receives an identical 200 OK response with the JSON body {"message": "This is some data."}.
  • Explanation:
    • This request matches an entry in the cache that is still valid.
    • The response is served directly from the cache without calling the upstream server.

Example 3: Stale Cache Entry or Different Request

  • Input (simulated):
    • The TTL for the entry from Example 1 expires.
    • A GET request to http://example.com/data is made again.
    • The upstream server now responds with:
      • Status: 200 OK
      • Body: {"message": "Updated data."}
      • Headers: Content-Type: application/json
  • Output:
    • The client receives the 200 OK response with the new JSON body {"message": "Updated data."}.
  • Explanation:
    • The cache entry is considered stale (due to TTL expiration).
    • The request is forwarded to the upstream server.
    • The new response is used and re-cached.

Constraints

  • Cache TTL: The cache TTL will be a configurable parameter, but for testing purposes, assume a default of 60 seconds.
  • Cache Storage: Use an in-memory map for the cache.
  • Concurrency: Implement basic thread-safety for the cache using sync.RWMutex.
  • Response Body Size: Assume response bodies are not excessively large (e.g., < 1MB).
  • HTTP Client: You will likely need to use Go's standard net/http package for making requests to the upstream.

Notes

  • You'll need to create a http.Handler or middleware that wraps another http.Handler.
  • Consider how to store the http.Response (or relevant parts of it like status, headers, and body) in your cache. You may need to copy the response body to be able to read it for caching and then still have it available to be written to the client.
  • Think about how to generate a unique cache key from a request.
  • You will need to simulate an upstream HTTP server for testing your middleware. This could be a simple net/http server that you spin up.
  • The goal is to demonstrate the caching logic, not to build a production-ready, highly scalable caching system. Focus on correctness and clarity.
Loading editor...
go