philiprehberger-etag

Tests Gem Version Last updated

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:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT