philiprehberger-etag
ETag generation and conditional request helpers with Rack middleware
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem "philiprehberger-etag"
Or install directly:
gem install philiprehberger-etag
Usage
require "philiprehberger/etag"
etag = Philiprehberger::Etag.generate("Hello, World!")
# => "\"dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f\""
Custom Hash Algorithm
require "philiprehberger/etag"
Philiprehberger::Etag.generate("content", algorithm: :sha256) # default
Philiprehberger::Etag.generate("content", algorithm: :sha512)
Philiprehberger::Etag.generate("content", algorithm: :md5)
Philiprehberger::Etag.generate("content", algorithm: :sha1)
Weak ETags
require "philiprehberger/etag"
weak = Philiprehberger::Etag.weak("Hello, World!")
# => "W/\"65a8e27d8879283831b664bd8b7f0ad4\""
Conditional Request Matching
require "philiprehberger/etag"
etag = Philiprehberger::Etag.generate("content")
# Weak comparison (If-None-Match)
Philiprehberger::Etag.match?(etag, etag) # => true
Philiprehberger::Etag.match?(etag, "*") # => true
Philiprehberger::Etag.match?(etag, "\"other\"") # => false
# Strong comparison (If-Match)
Philiprehberger::Etag.strong_match?(etag, etag) # => true
Direct Comparison
Compare two ETag strings using weak semantics (the W/ prefix is ignored):
Philiprehberger::Etag.equal?('"abc"', 'W/"abc"') # => true
Philiprehberger::Etag.equal?('"abc"', '"def"') # => false
Modified Detection
require "philiprehberger/etag"
etag = Philiprehberger::Etag.generate("content")
headers = { "HTTP_IF_NONE_MATCH" => etag }
Philiprehberger::Etag.modified?(etag, headers) # => false
headers = { "HTTP_IF_NONE_MATCH" => "\"stale\"" }
Philiprehberger::Etag.modified?(etag, headers) # => true
If-Modified-Since Support
require "philiprehberger/etag"
last_modified = Time.utc(2026, 3, 28, 12, 0, 0)
Philiprehberger::Etag.modified_since?(last_modified, "Fri, 27 Mar 2026 12:00:00 GMT")
# => true (resource is newer)
Philiprehberger::Etag.not_modified_since?(last_modified, "Sun, 29 Mar 2026 12:00:00 GMT")
# => true (resource is older)
File-Based ETags
require "philiprehberger/etag"
etag = Philiprehberger::Etag.for_file("/path/to/file.txt")
# => "\"a1b2c3...\"" (based on mtime + size, does not read content)
etag = Philiprehberger::Etag.for_file("/path/to/file.txt", algorithm: :md5)
ETag Parsing
require "philiprehberger/etag"
Philiprehberger::Etag.parse('"abc123"')
# => { weak: false, value: "abc123" }
Philiprehberger::Etag.parse('W/"abc123"')
# => { weak: true, value: "abc123" }
Philiprehberger::Etag.parse('"aaa", W/"bbb", "ccc"')
# => [{ weak: false, value: "aaa" }, { weak: true, value: "bbb" }, { weak: false, value: "ccc" }]
Rack Middleware
# config.ru
require "philiprehberger/etag"
use Philiprehberger::Etag::Middleware
run MyApp
The middleware computes a strong ETag from the raw response body before any Content-Encoding is applied, adds the ETag header, and returns 304 Not Modified with an empty body when If-None-Match matches.
API
| Method | Description |
|---|---|
Etag.generate(content, algorithm: :sha256) |
Strong ETag using specified algorithm, returns quoted string |
Etag.weak(content) |
Weak ETag from MD5, returns W/"..." string |
Etag.match?(etag, header) |
Weak comparison against If-None-Match header |
Etag.equal?(a, b) |
Compare two ETag strings with weak semantics (strips W/) |
Etag.strong_match?(etag, header) |
Strong comparison against If-Match header |
Etag.modified?(etag, request_headers) |
Check if resource is modified based on ETag headers |
Etag.modified_since?(last_modified, header) |
Check if resource was modified after If-Modified-Since date |
Etag.not_modified_since?(last_modified, header) |
Inverse of modified_since? |
Etag.for_file(path, algorithm: :sha256) |
Strong ETag from file mtime and size without reading content |
Etag.parse(header) |
Parse ETag header into {weak:, value:} hash or array of hashes |
Etag::Middleware.new(app) |
Rack middleware for automatic ETag and 304 handling |
Development
bundle install
bundle exec rspec
bundle exec rubocop
Support
If you find this project useful: