ratelimit

package
v2.51.0 Latest Latest
Warning

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

Go to latest
Published: Sep 21, 2026 License: MIT Imports: 16 Imported by: 0

README

ratelimit

Package ratelimit is a rules-based rate limiter backed by Redis fixed-window counters. It maintains a counter per (limiter, rule, characteristics) tuple and decides whether each request is within the configured limit.

It implements the cross-SDK rate-limit contract pinned by the labkit-spec conformance suite: every LabKit SDK uses the same Redis key shape and the same Lua scripts, so services in any language can enforce shared limits against the same Redis, and all emit the same gitlab_labkit_rate_limiter_* Prometheus metrics.

Usage

A Limiter is the unit of configuration for one call site (e.g. "rack requests", "graphql mutations"). Construct it once at boot and reuse it:

limiter, err := ratelimit.NewWithConfig(&ratelimit.Config{
	Name:  "api_request",
	Store: redisstore.New(redisClient.Redis()), // labkit redis client, or any goredis.UniversalClient
	Rules: []ratelimit.Rule{
		{
			Name:            "api_user",
			Limit:           600,
			Period:          time.Minute,
			Characteristics: []string{"user"},
			Match:           map[string]ratelimit.Matcher{"endpoint": ratelimit.Re(`^/api/`)},
		},
		{
			Name:            "unauthenticated",
			Limit:           60,
			Period:          time.Minute,
			Characteristics: []string{"ip"},
			Match:           map[string]ratelimit.Matcher{"user": ratelimit.Eq("")},
		},
	},
	Registerer: app.Metrics().Registerer(),
	Logger:     logger,
})

Every request calls Check:

result := limiter.Check(ctx, ratelimit.Identifier{
	"user":     userID,
	"ip":       clientIP,
	"endpoint": r.URL.Path,
})

if result.Blocked() {
	for k, v := range result.ResponseHeaders() {
		w.Header().Set(k, v)
	}
	http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
	return
}

The endpoint identifier key is treated specially: its query string is stripped, so URLs that vary only by query parameter share the same counter. A missing identifier key reads as "", which is how Eq("") matches unauthenticated traffic.

NewWithConfig validates the limiter name and every rule and returns an error instead of constructing a limiter that would misbehave at request time.

Rule

field meaning
Name Stable identifier used in Redis keys and metric labels. Must match ^[a-z0-9_]+$, max 64 chars. Renaming a rule abandons its in-flight counters.
Match Map of identifier key predicates that must all be satisfied for the rule to apply. Empty map matches anything.
Limit / LimitFunc Request threshold per period. The func form resolves on every check and receives the rule context.
Period / PeriodFunc Window length, whole seconds, minimum 1s.
Action ActionLimit (default), ActionLog, or ActionSkip. See Actions.
Characteristics Identifier keys folded into the Redis counter key. Each unique combination gets its own counter. Missing values encode as _unknown_.
CountDistinct Optional identifier key. When set, the rule counts distinct values seen for that key within the window, backed by a Redis SET. Must not overlap Characteristics.
BanFor / BanForFunc Optional ban duration, whole seconds, minimum 1s. On crossing the limit the rule writes a ban that outlives the counter window and stops counting while it holds. Action still decides who is blocked. Rejected on skip rules and with CountDistinct.
Matchers
constructor semantics
Eq(value) exact string equality. Eq("") matches a missing key
Re(pattern) RE2 regular expression, compiled by NewWithConfig, source capped at 200 chars. RE2 is linear-time, so no match timeout is needed
OneOf(values…) exact string membership
Actions

The rule's action describes what the rule does. The result describes the outcome, which is only ever allow or block.

rule action what it does exceeded outcome result label terminating?
ActionLimit count against the limit no allow allow no
ActionLimit count against the limit yes block block yes
ActionLog count against the limit (observability only) no allow allow no
ActionLog count against the limit (observability only) yes allow log no
ActionSkip don't count (bypass) n/a allow skip yes

A rule carrying a ban reports banned instead of block/log while the ban holds.

Evaluation flow

Check walks the rule list in order and evaluates every rule that matches: each one increments its own counter. Only two things terminate the walk: a matched skip rule, and an ActionLimit rule over its limit (rules declared after it are neither counted nor evaluated).

The returned Result collects one Evaluation per counted rule, and its readers (Rule, Info, Exceeded, ResponseHeaders) report the most constraining one: a blocking evaluation first, then exceeded ones, then the fewest remaining, ties broken by declaration order.

Dynamic configuration

LimitFunc, PeriodFunc, and BanForFunc resolve on every check. Pass per-request context with WithRuleContext. The concrete type is a contract between the rule's func and the call site:

result := limiter.Check(ctx, id, ratelimit.WithRuleContext(namespaceSettings))

WithCost supports cost-mode counters such as resource-usage limits. WithCost(0) charges nothing but still opens the fixed window on a fresh key, so a later charged request inherits it. Use Peek to read without writing.

Peek and Clear

Peek returns the same Result shape as Check without mutating Redis or extending any TTL - useful when one code path should account for the request and another should only gate on whether the caller is already over-limit.

Clear deletes this limiter's counters and bans for one identifier and returns the number of keys removed. It exists for call sites where a later success should wipe earlier failures, such as an authentication ban cleared by a valid login.

Redis keys

labkit:rl:{<limiter>:<rule>:<char>:<value>[:<char>:<value>...]}       counter
labkit:rl:{<limiter>:<rule>:<char>:<value>[:<char>:<value>...]}:ban   ban

The braces are a Redis Cluster hash tag: a rule's counter and ban share a slot (the ban script touches both in one call) while different rules spread across nodes. Characteristic values longer than 200 characters are replaced with a SHA-256 hexdigest. The TTL is set on the first write of each window and not extended afterwards, so the window is a true fixed window starting at the first request.

Each check is a single EVALSHA running the increment, SADD, or ban script. The whole read-modify-write runs atomically inside Lua, so no key can be left without a TTL and no concurrent check can miss a ban threshold crossing.

Fail-open

Check and Peek never return an error. Any failure - unreachable Redis, an unresolvable dynamic value - is logged at WARN with error_type: "rate_limit_error", counted with error="true" on checks_total or peeks_total, and returned as an allowed Result with Failed() == true. A request is never blocked on a partial evaluation.

The package does not bound the Redis call itself. A hung Redis costs each check the client's read timeout (3s by default in go-redis) before it fails open, so set timeouts on the client passed to redisstore.New.

A matched CountDistinct rule whose identifier is missing the distinct key fails open per-rule: the rule is skipped, the check completes, and the result reports Degraded() == true.

Metrics

metric type labels meaning
gitlab_labkit_rate_limiter_checks_total counter rate_limiter, action, matched, error Exactly one increment per Check call, including fail-open.
gitlab_labkit_rate_limiter_rule_evaluations_total counter rate_limiter, rule, action, result One increment per evaluated rule (plus one per matched skip rule).
gitlab_labkit_rate_limiter_peeks_total counter rate_limiter, error Exactly one increment per Peek call, including fail-open.
gitlab_labkit_rate_limiter_limit gauge rate_limiter, rule Resolved limit at the last check.
gitlab_labkit_rate_limiter_period_seconds gauge rate_limiter, rule Resolved period at the last check.

sum by (rate_limiter) (rate(gitlab_labkit_rate_limiter_checks_total[5m])) is the request rate through a limiter.

Storage

The limiter talks to Redis through the Store interface, two methods in plain Go types: Eval for the Lua scripts and Del for Clear. redisstore.New adapts any go-redis UniversalClient, including the labkit redis package's Client.Redis(). The adapter is the only place go-redis appears, so a consumer with another client, or a test double, implements Store and never imports go-redis.

Testing

Use testing/redistest (miniredis) for the Redis dependency and testing/metricstest for metric assertions:

srv := redistest.New(t)
client := goredis.NewClient(&goredis.Options{Addr: srv.Addr()})
limiter, err := ratelimit.NewWithConfig(&ratelimit.Config{Name: "test", Rules: rules, Store: redisstore.New(client)})

srv.Miniredis().FastForward(d) advances the fake clock to test window expiry and bans.

Documentation

Overview

Package ratelimit provides rules-based rate limiting backed by Redis fixed-window counters. The LabKit SDKs use identical Redis key shapes and Lua scripts, so services in any language can share limits against the same Redis, and all emit the same gitlab_labkit_rate_limiter_* Prometheus metrics.

A Limiter is configured once per call site with an ordered list of Rule values and a Store. The redisstore subpackage adapts a go-redis client, so this package itself never exposes go-redis types. Every request calls Limiter.Check to get back a Result describing what the caller should do:

limiter, err := ratelimit.NewWithConfig(&ratelimit.Config{
	Name:  "rack_request",
	Store: redisstore.New(redisClient.Redis()),
	Rules: []ratelimit.Rule{
		{
			Name:            "api_user",
			Limit:           600,
			Period:          time.Minute,
			Characteristics: []string{"user"},
			Match:           map[string]ratelimit.Matcher{"endpoint": ratelimit.Re(`^/api/`)},
		},
		{
			Name:            "unauthenticated",
			Limit:           60,
			Period:          time.Minute,
			Characteristics: []string{"ip"},
			Match:           map[string]ratelimit.Matcher{"user": ratelimit.Eq("")},
		},
	},
	Registerer: app.Metrics().Registerer(),
	Logger:     logger,
})

result := limiter.Check(ctx, ratelimit.Identifier{
	"user":     userID,
	"ip":       clientIP,
	"endpoint": r.URL.Path,
})
if result.Blocked() {
	for k, v := range result.ResponseHeaders() {
		w.Header().Set(k, v)
	}
	http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
	return
}

Evaluation

Check walks the rule list in order and evaluates every rule that matches. Each one increments its own counter. Only two things terminate the walk: a matched ActionSkip rule (bypass, nothing counted), and an ActionLimit rule over its limit (the request is rejected, so later rules are neither counted nor evaluated). The Result reports the most constraining evaluation: a blocking one first, then exceeded ones, then the fewest remaining, ties broken by declaration order.

Fail-open

Check and Peek never return an error. Any failure - unreachable Redis, an unresolvable dynamic value - is logged at WARN, counted with error="true" on checks_total or peeks_total, and returned as an allowed Result with Result.Failed true. A request is never blocked on a partial evaluation.

Windows

Counters are fixed windows starting at the first request: the TTL is set on the first write of each window and not extended on later increments. Bans (Rule.BanFor) write a second key that outlives the counter window and suppress counting while they hold, so a banned caller cannot extend its own ban.

See the package README for the full rule, action, and metrics reference.

Index

Constants

View Source
const EndpointKey = "endpoint"

EndpointKey is the identifier key treated specially by the limiter: its query string is stripped before matching and key building, so URLs that vary only by query parameter share the same counter.

Variables

View Source
var (
	ErrMissingStore = errors.New("store missing")
	ErrInvalidName  = errors.New("invalid limiter name")
	ErrInvalidRule  = errors.New("invalid rule")
)

Functions

This section is empty.

Types

type Action

type Action string

Action describes what a rule does when it matches. The zero value is treated as ActionLimit.

const (
	// ActionLimit enforces the rule: the result blocks when the rule is over
	// its limit, which also terminates evaluation.
	ActionLimit Action = "limit"
	// ActionLog counts and reports only. It never blocks and never
	// terminates evaluation. Use it for shadow rules during rollout.
	ActionLog Action = "log"
	// ActionSkip bypasses: a match permits the request and terminates
	// evaluation without any Redis operation, so Limit, Period,
	// Characteristics, and CountDistinct are inert on a skip rule.
	ActionSkip Action = "skip"
)

type CheckOption

type CheckOption func(*checkOptions)

CheckOption configures a single Limiter.Check or Limiter.Peek call.

func WithCost

func WithCost(cost float64) CheckOption

WithCost sets the amount added to the counter, defaulting to 1 (count-mode). Pass a non-1 value for cost-mode counters such as resource-usage limits. Passing 0 charges nothing but still opens the fixed window on a fresh key, so a later charged request inherits the window that started here. Use Limiter.Peek to read without writing. A negative cost would credit quota back, so the check fails open and logs instead. The cost is ignored by CountDistinct rules and by Limiter.Peek.

func WithRuleContext

func WithRuleContext(rctx RuleContext) CheckOption

WithRuleContext passes per-request context to func-resolved rule values (LimitFunc, PeriodFunc, BanForFunc). The concrete type contract is owned by the rule's func, not validated here: keep the rule definition and the call site colocated.

type Config

type Config struct {
	// Name identifies this limiter's call site (e.g. "rack_request",
	// "graphql_mutations"). Must match ^[a-z0-9_]+$. It is the first
	// segment of every Redis counter key for this limiter, so renaming a
	// limiter abandons any in-flight counters. Required.
	Name string

	// Rules is the ordered list of rules. Every rule whose Match
	// predicates are satisfied is evaluated and counted, and the Result
	// reports the most constraining one. Rule names must be unique within
	// a limiter.
	Rules []Rule

	// Store executes the limiter's Redis scripts and deletes. Wrap the
	// labkit redis client's Redis() value with redisstore.New, or supply
	// any [Store] implementation. Required.
	Store Store

	// Registerer publishes the limiter's Prometheus metrics. When nil,
	// metrics are not collected. Limiters share metric families and are
	// distinguished by the rate_limiter label, so any number of limiters
	// can register against the same Registerer.
	Registerer prometheus.Registerer

	// Logger emits structured warnings (fail-open errors, missing
	// count_distinct values). When nil, no log output is produced. The
	// limiter name is automatically included on every entry.
	Logger *slog.Logger
}

Config configures a Limiter created via NewWithConfig.

type Evaluation

type Evaluation struct {
	// Exceeded reports whether the post-increment count exceeded the
	// resolved limit. On a rule carrying a ban it tracks the ban instead:
	// once the counter window has expired the count can sit below the
	// limit while the ban still holds.
	Exceeded bool
	// Info holds the per-window counters.
	Info Info
	// contains filtered or unexported fields
}

Evaluation is the outcome of counting one matched rule.

func (*Evaluation) Blocks

func (e *Evaluation) Blocks() bool

Blocks reports whether this evaluation decides the request: an ActionLimit rule over its limit (or with its ban in force).

func (*Evaluation) Rule

func (e *Evaluation) Rule() Rule

Rule returns a copy of the evaluated rule. Mutating it has no effect on the limiter.

type Identifier

type Identifier map[string]string

Identifier describes the caller of one request as key-value attributes (e.g. user, ip, endpoint). Rules match against these values and fold the keys named in Rule.Characteristics into the Redis counter key.

A missing key reads as the empty string: matchers see "" and counter keys encode it as the "_unknown_" sentinel shared across the SDKs.

type Info

type Info struct {
	// Limit is the resolved limit for this check.
	Limit int64
	// Period is the resolved window length for this check.
	Period time.Duration
	// Count is the post-increment counter value. Integer-valued for
	// default cost=1 callers, fractional for cost-mode callers.
	Count float64
	// Remaining is the amount left before the limit is hit, floored at 0.
	Remaining float64
	// ResetAt is the best-effort UTC time when the counter window resets
	// (or, while a ban holds, when the ban lifts). Advisory only.
	ResetAt time.Time
}

Info holds the per-window counter snapshot for one evaluated rule.

type Limiter

type Limiter struct {
	// contains filtered or unexported fields
}

Limiter is the primary rate limiting API. Construct one per call site at application boot with NewWithConfig, then call Limiter.Check on every request. A Limiter is immutable after construction and safe for concurrent use.

func NewWithConfig

func NewWithConfig(cfg *Config) (*Limiter, error)

NewWithConfig returns a Limiter configured with cfg. It validates the limiter name and every rule (name format and uniqueness, action, period, ban and count-distinct constraints, match pattern compilation), returning an error rather than constructing a limiter that would misbehave at request time.

func (*Limiter) Check

func (l *Limiter) Check(ctx context.Context, id Identifier, opts ...CheckOption) *Result

Check evaluates the identifier against the limiter's rules, incrementing the counter of every matching rule, and returns a Result describing what the caller should do. It never returns an error: any failure (unreachable Redis, unresolvable dynamic configuration) fails open, logged at WARN and visible on the Result via Result.Failed.

func (*Limiter) Clear

func (l *Limiter) Clear(ctx context.Context, id Identifier) int64

Clear discards this limiter's state for one identifier: every rule's counter, and any ban written by a rule carrying a ban duration. It returns the number of Redis keys removed, or on error the number removed before it failed.

Counters otherwise only expire with their window, so this is the only way to end one early. It exists for call sites where a later success should wipe earlier failures, such as an authentication ban cleared by a valid login. It is scoped to this limiter and identifier, not to a single rule: a caller clearing after a success knows who succeeded, not which rules matched.

func (*Limiter) Name

func (l *Limiter) Name() string

Name returns the limiter name for use in logs and error messages.

func (*Limiter) Peek

func (l *Limiter) Peek(ctx context.Context, id Identifier, opts ...CheckOption) *Result

Peek reads the current rate-limit state without incrementing any counter or extending any TTL. It mirrors Limiter.Check otherwise. Useful for "have we already throttled this caller?" checks where another path does the actual increment.

When the underlying Redis key does not exist yet, the result reports count=0 and not exceeded, and the rule still matches. On error the result fails open identically to Check.

A CountDistinct rule needs no CountDistinct key on the identifier here: Peek reads the set's cardinality across all members. So Peek can report Result.Blocked for an identifier on which Check would report Result.Degraded.

type Matcher

type Matcher struct {
	// contains filtered or unexported fields
}

Matcher is a single key predicate in a Rule Match map. Build one with Eq, Re, or OneOf. The zero value behaves like Eq(""), which matches a missing or empty identifier value.

Glob and prefix matchers are intentionally out of scope. See gitlab-com/gl-infra/production-engineering#28853.

func Eq

func Eq(v string) Matcher

Eq returns a Matcher satisfied when the identifier value equals v. A missing identifier key reads as the empty string, so Eq("") matches callers that do not carry the key at all (e.g. unauthenticated traffic without a user).

func OneOf

func OneOf(values ...string) Matcher

OneOf returns a Matcher satisfied when the identifier value is one of values (exact string membership).

func Re

func Re(pattern string) Matcher

Re returns a Matcher satisfied when the identifier value matches the regular expression pattern. The pattern is compiled by NewWithConfig using Go's RE2 engine, which is linear-time, so no match timeout is needed. An invalid pattern or one longer than 200 characters makes NewWithConfig return an error.

type Result

type Result struct {
	// contains filtered or unexported fields
}

Result is the return value of Limiter.Check and Limiter.Peek.

It accumulates one Evaluation per matched-and-counted rule. The reader methods report the single most constraining one. A matched skip rule or a fail-open error replaces the evaluations as the source of the reported outcome.

func (*Result) Allowed

func (r *Result) Allowed() bool

Allowed is the complement of Result.Blocked. It covers everything else, including an exceeded log rule (visible via Result.Exceeded), a matched skip rule, no rule matched, and fail-open errors.

func (*Result) Blocked

func (r *Result) Blocked() bool

Blocked reports whether the caller should reject the request (e.g. with HTTP 429): an ActionLimit rule matched and is over its limit.

func (*Result) Degraded

func (r *Result) Degraded() bool

Degraded reports whether a matched CountDistinct rule was skipped because the identifier was missing its CountDistinct key. Unlike Result.Failed, the check still completed.

func (*Result) Evaluations

func (r *Result) Evaluations() []Evaluation

Evaluations returns every counted evaluation, in rule declaration order.

func (*Result) Exceeded

func (r *Result) Exceeded() bool

Exceeded reports whether the reported evaluation's counter exceeded its limit. A log rule over its limit reports true here while the result still allows.

func (*Result) Failed

func (r *Result) Failed() bool

Failed reports whether the check failed open: Redis or a rule's dynamic configuration was unavailable, so the request is allowed without a verdict.

func (*Result) Info

func (r *Result) Info() *Info

Info returns the per-window counters for the reported rule. Nil when nothing matched, on fail-open, or when the matched rule is a skip rule (no counter exists).

func (*Result) Matched

func (r *Result) Matched() bool

Matched reports whether at least one rule's match conditions were satisfied.

func (*Result) ResponseHeaders

func (r *Result) ResponseHeaders() map[string]string

ResponseHeaders returns RFC-compliant rate limit response headers, or an empty map when no rule matched or an error occurred, so it is safe to merge unconditionally. Keys: RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (Unix timestamp). Remaining is truncated to an integer: the RateLimit header spec requires integer values.

func (*Result) Rule

func (r *Result) Rule() *Rule

Rule returns a copy of the reported rule: the skip rule when one matched, otherwise the most constraining evaluated rule. Nil when nothing matched. Other rules may also have matched and been counted. See Result.Evaluations. Mutating the copy has no effect on the limiter.

func (*Result) Skipped

func (r *Result) Skipped() bool

Skipped reports whether a matched skip rule terminated evaluation.

type Rule

type Rule struct {
	// Name is a stable identifier used in Redis keys and metric labels.
	// Must match ^[a-z0-9_]+$, max 64 characters. Renaming a rule abandons
	// its in-flight counters.
	Name string

	// Match holds identifier key predicates that must all be satisfied for
	// the rule to apply. An empty or nil map matches any identifier.
	Match map[string]Matcher

	// Limit is the request threshold per Period. Must be at least 1 unless
	// LimitFunc is set: an omitted Limit would block every matching
	// request. Ignored when LimitFunc is set.
	Limit int64

	// LimitFunc, when set, resolves the limit on every check, receiving the
	// [RuleContext] passed via [WithRuleContext] (nil when none was passed).
	// A resolved value of 0 or less blocks every matching request, which
	// lets a dynamic setting mean "deny all".
	LimitFunc func(rctx RuleContext) int64

	// Period is the fixed window length. Must be at least one second. The
	// window TTL is written in whole seconds, so any sub-second remainder
	// is dropped. Ignored when PeriodFunc is set.
	Period time.Duration

	// PeriodFunc, when set, resolves the period on every check. A resolved
	// period under one second is an error and fails open.
	PeriodFunc func(rctx RuleContext) time.Duration

	// Action is what the rule does when it matches. Defaults to
	// [ActionLimit].
	Action Action

	// Characteristics names the identifier keys whose values are folded
	// into the Redis counter key. Each unique combination of values gets
	// its own counter. Missing or empty values are encoded as "_unknown_".
	Characteristics []string

	// CountDistinct optionally names an identifier key. When set, the rule
	// counts the number of distinct values seen for that key within the
	// (characteristics-bucketed) window, backed by a Redis SET rather than
	// a counter. Must not overlap Characteristics. Cannot be combined with
	// a ban.
	CountDistinct string

	// BanFor is an optional ban duration, minimum 1s, truncated to whole
	// seconds like Period. On crossing the limit the rule writes a separate
	// ban key that outlives the counter window, and stops counting while the
	// ban holds. Action still decides who is blocked: ActionLimit enforces
	// the ban, ActionLog records what it would have done. Rejected on
	// ActionSkip. Ignored when BanForFunc is set.
	BanFor time.Duration

	// BanForFunc, when set, resolves the ban duration on every check. A
	// resolved duration under one second is an error and fails open, so a
	// rule whose ban duration cannot resolve stops blocking entirely.
	BanForFunc func(rctx RuleContext) time.Duration
}

Rule describes a single rate limit rule. Rules are value objects: NewWithConfig validates and copies them, so mutating a Rule after constructing a Limiter has no effect.

type RuleContext

type RuleContext = any

RuleContext carries optional per-check data into LimitFunc, PeriodFunc, and BanForFunc, letting rules resolve dynamic configuration (e.g. per-namespace settings) without rebuilding the Rule. The concrete type is a contract between the rule's func and the call site passing WithRuleContext. The limiter never inspects it and passes nil when the caller supplies none.

type Store

type Store interface {
	// Eval runs a Lua script against keys with args and returns its reply.
	// Implementations should load the script once and reference it by
	// digest afterwards (EVALSHA, falling back to EVAL on NOSCRIPT).
	//
	// Replies follow the Redis-to-Go mapping: integers as int64, bulk
	// strings as string, arrays as []any, and a nil reply as nil.
	Eval(ctx context.Context, script string, keys []string, args ...any) (any, error)

	// Del removes keys and returns how many of them existed.
	Del(ctx context.Context, keys ...string) (int64, error)
}

Store is the storage surface a Limiter needs, expressed in plain Go types so the storage client stays an implementation detail of the adapter. The go-redis adapter is redisstore.New. A test double needs no Redis client at all.

Directories

Path Synopsis
Package redisstore adapts a go-redis client to ratelimit.Store.
Package redisstore adapts a go-redis client to ratelimit.Store.

Jump to

Keyboard shortcuts

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