caddy-website/src/docs/markdown/extending-caddy/placeholders.md
a bcd6694039
Update src/docs/markdown/extending-caddy/placeholders.md
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-08-21 14:05:46 -05:00

4.4 KiB

title
Placeholder Support

Placeholders

In Caddy, placeholders are processed by each individual plugin themselves. They are not parsed at config time, but instead preserved and processed at runtime.

This means that if you wish for your plugin to support placeholders, you must explicitly add support for them.

If you are not yet familiar with placeholders, start by reading here!

Placeholder Internals

Placeholders are a string in the format {foo.bar} used as dynamic configuration values, which is later evaluated at runtime.

Placeholders-like strings which start with a dollar sign ({$FOO}), are evaulated at Caddyfile parse time, and do not need to be dealt with by your plugin. This is because these are not placeholders, but Caddyfile-specific environmental variable substitution, they just happen to share the {} syntax.

It is therefore important to understand that {env.HOST} is inherently different from something like {$HOST}

As an example, see the following caddyfile:

:8080 {
  respond {$HOST} 200
}

:8081 {
  respond {env.HOST} 200
}

When you adapt this Caddyfile with HOST=example caddy adapt you will get

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [
            ":8080"
          ],
          "routes": [
            {
              "handle": [
                {
                  "body": "example",
                  "handler": "static_response",
                  "status_code": 200
                }
              ]
            }
          ]
        },
        "srv1": {
          "listen": [
            ":8081"
          ],
          "routes": [
            {
              "handle": [
                {
                  "body": "{env.HOST}",
                  "handler": "static_response",
                  "status_code": 200
                }
              ]
            }
          ]
        }
      }
    }
  }
}

Importantly, look at the "body" field in both srv0 and srv1.

Since srv0 used {$HOST}, the special environmental variable replacement with $, the value became example, as it was processed during Caddyfile parse time.

Since srv1 used {env.HOST}, a normal placeholder, it was parsed as its own raw string value, {env.HOST}

Some users may immediately notice that this means it is impossible to use the {$ENV} syntax in a JSON config. The solution to this is to process such placeholders at Provision time, which is covered below.

How to use placeholders in your plugin

Parse the raw placeholder value in your unmarshaler

Placeholders are not evaluated at Caddyfile parse time, and should be preserved for later use. They are used as their raw string values.

In other words, parsing a placeholder is no different from parsing any other string.

func (g *Gizmo) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
	d.Next()
	if !d.Args(&g.Name) {
		// not enough args
		return d.ArgErr()
	}
}

Evaluate the placeholder during Match or Serve

In order to now correctly read our g.Name placeholder in a plugin matcher or middleware, we must extract the replacer from the context and use that replacer on our saved placeholder string.

This gives us a string with all valid replacements done, which we can then use in whichever way we want. In the example, we write those bytes to output

func (g *Gizmo) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
	_, err := w.Write([]byte(repl.ReplaceAll(g.Name,"")))
	if err != nil {
		return err
	}
	return next.ServeHTTP(w, r)
}

Alternatively, resolve the placeholder during Provision

If you only use global placeholders, like env, then you may initialize a global replacer at provision time, and use it to replace such values. This also allows users of config file formats other than Caddyfile to use environmental variables.

func (g *Gizmo) Provision(ctx caddy.Context) error {
	repl := caddy.NewReplacer()
	g.Name = repl.ReplaceAll(g.Name,"")
	return nil
}


func (g *Gizmo) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
	// in this case, you don't need to replace at serve-time anymore
	_, err := w.Write([]byte(g.Name))
	if err != nil {
		return err
	}
	return next.ServeHTTP(w, r)
}