Skip to content

Optimization: Implement ETag / 304 Not Modified for static endpoints & fix middleware bug #445

@ARCoder181105

Description

@ARCoder181105

Summary

Static GTFS endpoints (agencies, routes, stops, shapes) use Cache-Control: public, max-age=300 but have no conditional request support. Once max-age expires, clients are forced to re-download the full JSON payload even if the underlying GTFS dataset has not changed in days. This causes unnecessary bandwidth consumption and redundant JSON serialization on every cache expiry cycle.

While reviewing this, I also found a bug in caching_middleware.go that would silently break any ETag implementation added in the future, so both need to be addressed together.

1. ETag Support

A SHA-256 FileHash is already computed and stored on every GTFS import in gtfsdb/helpers.go — this can be used directly as the ETag value with no extra runtime hashing cost at all.

  • Proposed change: Expose FileHash on the Manager struct after a successful load in internal/gtfs/static.go, then add a new middleware for static endpoints that reads the incoming If-None-Match header.
  • If it matches the current hash, return 304 Not Modified with an empty body immediately, skipping all database queries and JSON serialization. If it does not match, proceed normally and write ETag: "<hash>" on the response.
  • Scope: This must be scoped to static tier endpoints only. Applying it to real-time endpoints like arrivals-and-departures-for-stop or vehicles-for-agency would incorrectly serve stale vehicle positions as unchanged, which is a correctness bug.

2. Middleware Bug

cacheControlWriter.WriteHeader in caching_middleware.go currently treats 304 as a non-2xx response and overwrites its Cache-Control header with no-cache, no-store, must-revalidate. This causes clients to discard their cached copy on every 304 response, completely defeating the purpose of conditional requests. The existing tests in caching_middleware_test.go only cover 200 and 404 responses so this has gone undetected.

// current — 304 incorrectly hits the else branch
if code >= 200 && code < 300 {
    w.Header().Set("Cache-Control", w.headerValue)
} else {
    w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
}

// fix
if (code >= 200 && code < 300) || code == http.StatusNotModified {
    w.Header().Set("Cache-Control", w.headerValue)
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions