etag

package
v1.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 19, 2026 License: Apache-2.0 Imports: 2 Imported by: 0

Documentation

Overview

Package etag provides automatic ETag generation and conditional response middleware for celeris.

The middleware computes a CRC-32 checksum of the response body and sets an ETag header. On subsequent requests with a matching If-None-Match header, it returns 304 Not Modified with no body, saving bandwidth.

Basic usage:

server.Use(etag.New())

Strong ETags (byte-for-byte identical guarantee):

server.Use(etag.New(etag.Config{Strong: true}))

Algorithm

The ETag value is computed using CRC-32 (IEEE polynomial) of the full response body. By default, weak ETags are generated in the format W/"xxxxxxxx" where xxxxxxxx is the hex-encoded checksum. Strong ETags omit the W/ prefix: "xxxxxxxx".

CRC-32 is chosen for speed (single pass, no allocations beyond the fixed-size stack buffer). It is not cryptographically secure, but ETag is a cache validation mechanism, not a security primitive.

Custom Hash Function

Use Config.HashFunc to supply a custom hash. The function receives the full response body and returns the opaque-tag string (without quotes or W/ prefix -- those are added automatically based on the Strong setting):

server.Use(etag.New(etag.Config{
    HashFunc: func(body []byte) string {
        h := sha256.Sum256(body)
        return hex.EncodeToString(h[:16])
    },
}))

Handler-Set ETags

If the downstream handler already sets an ETag header, the middleware respects it and does not recompute. The existing ETag is still checked against If-None-Match for conditional 304 responses.

Weak Comparison (RFC 7232 Section 2.3.2) RFC 7232 Section 2.3.2)" aria-label="Go to Weak Comparison (RFC 7232 Section 2.3.2)">¶

If-None-Match uses weak comparison: the W/ prefix is stripped before comparing opaque-tags. This means W/"abc" matches both W/"abc" and "abc", per the RFC.

If-None-Match: *

The wildcard value "*" matches any ETag, unconditionally returning 304.

If-Match (Not Supported)

This middleware does not handle If-Match (RFC 7232 Section 3.1). If-Match is used for conditional writes (PUT/DELETE) and should be validated at the application layer. The middleware only processes GET/HEAD requests.

If-None-Match Lenient Parsing

The If-None-Match parser accepts unquoted tokens in addition to properly quoted ETag values. While RFC 7232 specifies that entity-tags must be quoted strings, real-world clients and proxies occasionally emit bare tokens. The parser handles these gracefully to avoid spurious cache misses.

Method Filtering

Only GET and HEAD requests are processed. POST, PUT, DELETE, PATCH, and other methods bypass the middleware entirely (no buffering, no ETag computation) with zero allocations.

304 Not Modified Response

When returning 304, the middleware sends only the ETag header and any other response headers set by the handler. Content-Length is not included because the buffered response body (from which Content-Length would be derived) is discarded before the 304 is sent. This is RFC-compliant -- Content-Length is optional on 304 responses (RFC 9110 Section 8.6).

StreamWriter Incompatibility

This middleware uses celeris.Context.BufferResponse which is incompatible with celeris.Context.StreamWriter. If the downstream handler uses StreamWriter, BufferResponse returns nil and the streamed response bypasses this middleware entirely. Use Config.Skip to explicitly exclude streaming endpoints.

Full-Body Buffering

The middleware buffers the entire response body in memory to compute the ETag checksum. For large responses, this increases memory usage. Use Config.Skip or Config.SkipPaths to bypass ETag processing for endpoints that return large payloads (file downloads, streaming):

server.Use(etag.New(etag.Config{
    SkipPaths: []string{"/download", "/export"},
}))

Middleware Ordering

ETag should run INSIDE compression middleware (closer to the handler). This ensures the ETag is computed on the uncompressed body, so clients that re-request without Accept-Encoding still get a cache hit:

server.Use(compress.New())  // outer: compresses the response
server.Use(etag.New())      // inner: ETag on uncompressed body

Skipping

Use Config.Skip for dynamic skip logic or Config.SkipPaths for path exclusions. SkipPaths uses exact path matching:

server.Use(etag.New(etag.Config{
    SkipPaths: []string{"/health", "/metrics"},
}))

Non-2xx and Empty Responses

Responses with non-2xx status codes or empty bodies are flushed without ETag computation. Only successful responses with content participate in conditional caching.

Session Middleware Interaction

When session middleware runs alongside ETag, 304 Not Modified responses still carry the Set-Cookie header from session refresh. This is correct behavior (sessions must be refreshed for security), but it prevents shared caches (CDN, reverse proxy) from caching 304 responses per RFC 7234 Section 3.1. Browser-level caching is unaffected.

For static assets that benefit from shared-cache ETag optimization, consider using SkipPaths on the session middleware to exclude those paths.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func New

func New(config ...Config) celeris.HandlerFunc

New creates an ETag middleware with the given config.

Composition: when a downstream handler or middleware (e.g. middleware/static) already sets an "etag" response header, this middleware reuses that tag instead of computing its own. The body hash is only computed when no ETag is present. This means etag(static(...)) emits exactly one ETag header — the mtime/size form static computed — and no double tagging occurs.

Example
package main

import (
	"github.com/goceleris/celeris/middleware/etag"
)

func main() {
	// Weak ETags (default) -- suitable for responses that may be
	// content-negotiated or transfer-encoded.
	// s := celeris.New()
	// s.Use(etag.New())
	_ = etag.New()
}
Example (HashFunc)
package main

import (
	"crypto/sha256"
	"encoding/hex"

	"github.com/goceleris/celeris/middleware/etag"
)

func main() {
	// Use SHA-256 (truncated) instead of the default CRC-32.
	_ = etag.New(etag.Config{
		HashFunc: func(body []byte) string {
			h := sha256.Sum256(body)
			return hex.EncodeToString(h[:16])
		},
	})
}
Example (Skip)
package main

import (
	"strings"

	"github.com/goceleris/celeris"
	"github.com/goceleris/celeris/middleware/etag"
)

func main() {
	// Dynamically skip ETag for server-sent event streams.
	_ = etag.New(etag.Config{
		Skip: func(c *celeris.Context) bool {
			return strings.HasPrefix(c.Path(), "/events")
		},
	})
}
Example (SkipPaths)
package main

import (
	"github.com/goceleris/celeris/middleware/etag"
)

func main() {
	// Skip ETag computation for large-payload endpoints.
	_ = etag.New(etag.Config{
		SkipPaths: []string{"/download", "/export", "/stream"},
	})
}
Example (Strong)
package main

import (
	"github.com/goceleris/celeris/middleware/etag"
)

func main() {
	// Strong ETags -- byte-for-byte identical guarantee.
	_ = etag.New(etag.Config{Strong: true})
}

Types

type Config

type Config struct {
	// Skip defines a function to skip this middleware for certain requests.
	Skip func(c *celeris.Context) bool

	// SkipPaths lists paths to skip (exact match).
	SkipPaths []string

	// Strong controls whether ETags use the strong format "xxxxxxxx".
	// When false (default), weak ETags are used: W/"xxxxxxxx".
	// Weak ETags are recommended for responses that may be content-negotiated
	// or transfer-encoded.
	Strong bool

	// HashFunc computes a custom ETag from the response body.
	// When set, the returned string is used as the opaque-tag (without quotes
	// or W/ prefix -- those are added automatically based on the Strong setting).
	// Default: CRC-32 IEEE hex.
	HashFunc func(body []byte) string
}

Config defines the ETag middleware configuration.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL