Layer Patterns
Lets one layer configuration answer for many layer names. Parts of the name become placeholder values providers can use, such as to pick an upstream layer.
| This page explains how patterns behave. See Layer for the full list of layer options. |
Matching a layer name
Layers in tilegroxy can be matched to incoming requests in one of two ways. The default method is to match the incoming layer name (from the URL) with the layer’s configured id. A layer can instead define a pattern.
Incoming tile requests look like /tiles/{layerName}/{z}/{x}/{y}. Tilegroxy checks each configured layer in the order they appear in the configuration and uses the first one that matches layerName:
-
A layer without a
patternmatches whenlayerNameequals itsid. -
A layer with a
patternmatches whenlayerNamefits the pattern, every placeholder value passes itsparamValidatorexpression, and every placeholder inparamMaphas a listed value or a default. Theiditself is not checked.
Because the first match wins, put specific layers above broad patterns that could also match their names. A request that matches nothing is rejected as an unknown layer.
A pattern layer still needs a unique id as it’s used for the layer’s identity elsewhere.
The ref provider resolves its layer the same way as an incoming request, so a ref can point at a name that fits a pattern.
Placeholder syntax
A placeholder is a name wrapped in curly braces. Everything outside the braces is literal text that must appear in the layer name exactly.
pattern: my_{name}_{version}
Given a request for my_first_v2, name is first and version is v2.
Placeholders must be "non-subsequent", meaning two placeholders can’t sit directly next to each other. There must be some literal text between them, because that text is how tilegroxy knows where one value ends and the next begins. A pattern such as a_{x}{y} fails at startup. The same placeholder name also can’t be used twice in one pattern.
Matching is simple. Each value runs until the first occurrence of the literal text that follows it. With the pattern above, my_a_b_v2 matches with name as a and version as b_v2. Keep this in mind when a value could contain your separator, and use paramValidator to reject names that would split in an unexpected way. Without a validator a placeholder can also match an empty string.
Validating values
paramValidator maps a placeholder name to a regular expression that its value must match. Expressions are anchored automatically, so they must match the whole value rather than just part of it. Use the key * to apply an expression to every placeholder. A name with any value failing its expression doesn’t match the layer at all, so the request moves on to later layers and is rejected if nothing else matches.
pattern: my_{name}_{version}
paramValidator:
"*": "[a-zA-Z0-9]+"
version: "v[0-9]{1,3}"
paramValidator only applies to layers that have a pattern.
Mapping values
In some cases you might want to have different values in your URLs compared to what is needed for your upstream provider. In those cases you can use paramMap to perform simple replacements from one value to another.
-
YAML
-
JSON
- id: flood
pattern: flood_{type}_{period}
paramValidator: (1)
period: "[0-9]{6}|latest" (2)
paramMap:
type:
- from: river (3)
to: RiverFlood_v2
- from: coastal
to: CoastalFlood_v3
- from: sea (4)
to: CoastalFlood_v3
period:
- from: latest (5)
to: "202410"
- default: true (6)
cacheVersion: auto (7)
provider:
name: proxy
url: https://tiles.example.com/{layer.type}/{layer.period}/{z}/{x}/{y} (8)
| 1 | Since period allows defaults in paramMap, it needs an explicit validator to ensure dangerous values aren’t injected. |
| 2 | paramValidator checks the value from the request before paramMap replaces it, so latest has to be allowed here. |
| 3 | from values must match exactly, including case. A type that isn’t listed doesn’t match the layer, which makes the list an exact allowlist. |
| 4 | Several values can map to the same upstream value. |
| 5 | Clients can ask for latest without knowing the newest period. Changing to moves every client to a new period at once. |
| 6 | Any other period is used as is. With a to on this entry every other period would be replaced by that value instead. |
| 7 | Having cache version set to auto means the cache is invalidated whenever latest changes. |
| 8 | Providers only see the mapped value, such as RiverFlood_v2 rather than river. |
[
{
"id": "flood",
"pattern": "flood_{type}_{period}",
"paramValidator": { (1)
"period": "[0-9]{6}|latest" (2)
},
"paramMap": {
"type": [
{ "from": "river", "to": "RiverFlood_v2" }, (3)
{ "from": "coastal", "to": "CoastalFlood_v3" },
{ "from": "sea", "to": "CoastalFlood_v3" } (4)
],
"period": [
{ "from": "latest", "to": "202410" }, (5)
{ "default": true } (6)
]
},
"cacheVersion": "auto", (7)
"provider": {
"name": "proxy",
"url": "https://tiles.example.com/{layer.type}/{layer.period}/{z}/{x}/{y}" (8)
}
}
]
| 1 | Since period allows defaults in paramMap, it needs an explicit validator to ensure dangerous values aren’t injected. |
| 2 | paramValidator checks the value from the request before paramMap replaces it, so latest has to be allowed here. |
| 3 | from values must match exactly, including case. A type that isn’t listed doesn’t match the layer, which makes the list an exact allowlist. |
| 4 | Several values can map to the same upstream value. |
| 5 | Clients can ask for latest without knowing the newest period. Changing to moves every client to a new period at once. |
| 6 | Any other period is used as is. With a to on this entry every other period would be replaced by that value instead. |
| 7 | Having cache version set to auto means the cache is invalidated whenever latest changes. |
| 8 | Providers only see the mapped value, such as RiverFlood_v2 rather than river. |
An entry with default: true applies to every value the other entries don’t list. A default can optionally include the to option, if it’s omitted then the default is a straight passthrough.
Startup fails if a key isn’t a placeholder in the pattern, a from value is listed twice, a placeholder has more than one default, or a default has a from. Providers, including custom ones, and analytics see the mapped value. Tiles are still cached under the layer name from the request (before mappings).
Using values in providers
Providers reference a placeholder value as {layer.NAME}, where NAME is the placeholder name from the pattern. The following providers support it:
-
Proxy in
url. Values are percent-encoded, so a/in a value becomes%2Frather than a path separator. -
CGI in
uri, with the same escaping as Proxy. -
PostGIS MVT in
filter. Values are passed as prepared statement parameters, not pasted into the SQL. -
Ref in
layer. This also works in refs nested inside other providers, such as fallback or blend, and in blend’slayer.pattern.
Startup fails if a {layer.NAME} anywhere in the provider configuration names a placeholder that isn’t in the pattern, such as a typo. Names are case sensitive, so {layer.region} doesn’t match a pattern of {Region}.
Placeholder values are also available to analytics as the layerparams field and to custom providers through LayerParams.
Examples
A pattern layer has no single name, so features that need one use the examples list instead. This is currently used in a couple ways:
-
TileJSON lists one entry per example. Without examples the layer is left out of the listing.
-
tilegroxy testtests each example. Without examples the layer is skipped with a warning.
Caching
Tiles are cached under the layer name from the request, not the id. Every distinct set of placeholder values gets its own cache entries, which is usually what you want since they produce different tiles. It also means a pattern that accepts many values can fill a cache with many variations.
The disk, S3, and memcached caches replace any character other than letters, digits, -, and . with _ before using the name in a key. This keeps a request from escaping the cache directory or adding path segments to an object key. The tradeoff is that names differing only in those characters share entries: a b and a/b both become a_b. A tight paramValidator avoids this.
Security
Placeholder values come straight from the request, so they are User input and should be treated as hostile. Escaping in Proxy and CGI and prepared statements in PostGIS protect against the most direct injection, but they don’t stop a User from asking your upstream for something you didn’t intend to serve.
Always configure paramValidator or paramMap on a pattern layer whose values reach a provider, and make the expressions as tight as you can. Alphanumeric values of a known length are a good default. Never let a placeholder control the hostname of an upstream URL.
|
A warning is logged at startup and on every reload for each pattern layer with a placeholder that has no paramValidator expression (including *) and isn’t in paramMap. A paramMap whose default passes values through unchanged doesn’t count, since it accepts anything.
See Security for more.
Worked example
The NOAA post-storm example serves NOAA’s post-storm aerial imagery. NOAA publishes each flight as a separate tile set with a date and letter in the URL, such as 20230902a. Rather than one layer per flight, a single pattern layer covers them all:
-
YAML
-
JSON
- id: noaa_poststorm_imagery_in_parts
pattern: noaa_poststorm_{version}
paramValidator:
version: "[0-9]{8}[ab]"
examples:
- noaa_poststorm_20230902a
- noaa_poststorm_20230901b
client:
contenttypes:
- binary/octet-stream
- image/png
timeout: 2
provider:
name: fallback
primary:
name: proxy
url: https://stormscdn.ngs.noaa.gov/{layer.version}-rgb/{z}/{x}/{y}
secondary:
name: static
color: FFF0
[
{
"id": "noaa_poststorm_imagery_in_parts",
"pattern": "noaa_poststorm_{version}",
"paramValidator": {
"version": "[0-9]{8}[ab]"
},
"examples": [
"noaa_poststorm_20230902a",
"noaa_poststorm_20230901b"
],
"client": {
"contenttypes": [
"binary/octet-stream",
"image/png"
],
"timeout": 2
},
"provider": {
"name": "fallback",
"primary": {
"name": "proxy",
"url": "https://stormscdn.ngs.noaa.gov/{layer.version}-rgb/{z}/{x}/{y}"
},
"secondary": {
"name": "static",
"color": "FFF0"
}
}
}
]
A request for /tiles/noaa_poststorm_20230902a/16/18020/27142 matches the pattern with version set to 20230902a. That value is exactly eight digits and an a, so it passes the validator. The proxy then requests https://stormscdn.ngs.noaa.gov/20230902a-rgb/16/18020/27142. Flights only cover a small area, so the fallback returns a transparent tile wherever NOAA has none.
A request for noaa_poststorm_latest fails the validator and is rejected. So is noaa_poststorm_imagery_in_parts, since the id isn’t matched once a pattern is set.
The examples let tilegroxy test and TileJSON work with two real flights.
Other layers in the same file reuse the pattern layer to combine all flights for a storm into one layer:
-
YAML
-
JSON
- id: noaa_2023_lee
provider:
name: blend
mode: normal
providers:
- name: ref
layer: noaa_poststorm_20230917a
- name: ref
layer: noaa_poststorm_20230917b
- id: noaa_2023_idalia
provider:
name: blend
mode: normal
layer:
pattern: noaa_poststorm_{v}
values:
- v: 20230902a
- v: 20230901b
- v: 20230901a
- v: 20230831b
- v: 20230831a
[
{
"id": "noaa_2023_lee",
"provider": {
"name": "blend",
"mode": "normal",
"providers": [
{
"name": "ref",
"layer": "noaa_poststorm_20230917a"
},
{
"name": "ref",
"layer": "noaa_poststorm_20230917b"
}
]
}
},
{
"id": "noaa_2023_idalia",
"provider": {
"name": "blend",
"mode": "normal",
"layer": {
"pattern": "noaa_poststorm_{v}",
"values": [
{
"v": "20230902a"
},
{
"v": "20230901b"
},
{
"v": "20230901a"
},
{
"v": "20230831b"
},
{
"v": "20230831a"
}
]
}
}
}
]
noaa_2023_lee uses ref providers that name concrete flights. noaa_2023_idalia does the same with less repetition using blend’s layer option. Here {v} belongs to blend, not to a layer pattern, so it uses the bare placeholder name rather than {layer.v}.
Each referenced flight is cached separately under its own name, so a request for noaa_2023_idalia also warms the cache for the individual flights.