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-etagUsage
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) # => trueModified 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) # => trueIf-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 MyAppThe 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.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 rubocopSupport
If you find this project useful: