# `Otel.Propagator.TextMap.Baggage`
[🔗](https://github.com/yangbancode/otel/blob/main/lib/otel/propagator/text_map/baggage.ex#L1)

W3C Baggage propagator (W3C `HTTP_HEADER_FORMAT.md` §Header
Content L19-L113; OTel `context/api-propagators.md`
§TextMap Inject/Extract L155-L203).

Injects and extracts the `baggage` HTTP header. Wire format
per W3C §Definition L23-L41 (ABNF):

    baggage-string = list-member 0*179( OWS "," OWS list-member )
    list-member    = key OWS "=" OWS value *( OWS ";" OWS property )

Example header value:

    userId=abc123,serverNode=node-42;region=us-east

## Design notes

Five places worth calling out — three intentional
divergences from `opentelemetry-erlang`'s
`otel_propagator_baggage.erl`, one spec alignment that
Erlang has not yet made, and one acknowledged W3C-token
divergence on the wire. Each is documented so future
readers can see where we stand.

### 1. Strict RFC 3986 percent-encoding with U+FFFD replacement

W3C §value L64-L68 requires RFC 3986 percent-encoding.
Per §Definition L32, `baggage-octet` explicitly includes
`+` (0x2B) as a valid raw character, so `+` in a value
MUST mean literal plus — not an encoded space.

Encoding and decoding are delegated to
`Otel.Baggage.Percent`, which also implements the §L69
MUST that percent-encoded octet sequences not matching
UTF-8 must be replaced with `U+FFFD`. Inject produces
`%20` for space; extract decodes `%20` to space and leaves
`+` as literal, preserving round-trip fidelity.

`opentelemetry-erlang` (`otel_propagator_baggage.erl`
L146-L147) still uses `form_urlencode` with a `TODO: call
uri_string:percent_encode` comment — its encoder emits `+`
for space while its decoder treats `+` as literal, which
loses round-trip fidelity for space-containing values and
conflates `+` semantics. We do not mirror that limitation.

### 2. Metadata as opaque string

`Otel.Baggage` stores each entry's metadata as a single
string (`{value, metadata}`). W3C §property L82-L100 defines
a structured property list (e.g. `;k1=v1;k2;k3=v3`), and
`opentelemetry-erlang` parses it into a list of key/value
tuples with per-property percent-encoding.

This propagator round-trips the raw metadata string
byte-for-byte — no splitting on `;`, no per-property
percent-encoding. The choice mirrors `Otel.Baggage`'s
opaque-metadata design; callers who need structured
metadata parse it themselves.

### 3. Extract merges with existing baggage

`opentelemetry-erlang` replaces the context's baggage with
what the header carries (`otel_baggage:set_to`). We merge:
entries in the incoming header overwrite same-key entries
in the context, but entries only present in the context
are preserved.

Neither behaviour is mandated — W3C governs only the wire
format, and OTel L108-L114 says the returned context
"contains the extracted value" without prescribing how it
combines with pre-existing values. Merge serves the common
pattern of "local annotation + received baggage flowing
together".

### 4. W3C Limits not enforced at the propagator layer

W3C §Limits L102-L113 mandates propagating all list-members
when the result is ≤64 entries and ≤8192 bytes, and allows
(MAY) dropping entries otherwise. We always emit every
entry present in `Baggage.current/1`. Neither the MUST
(trivially satisfied for small baggage) nor the MAY
(optional) requires defensive limits here; if limits become
necessary they belong in `Otel.Baggage`'s mutation
surface, not the wire-format propagator.

### 5. Key encoding over-encodes RFC 7230 token characters

W3C `HTTP_HEADER_FORMAT.md` L52-L53 says baggage *names*
are RFC 7230 `token` values. RFC 7230 §3.2.6 `tchar`
permits sub-delim characters (`!`, `#`, `$`, `&`, `'`, `*`,
`+`, `-`, `.`, `^`, `_`, `` ` ``, `|`, `~`) in addition to
ALPHA/DIGIT — so a key like `user.id` or `user!id` is a
valid `token`.

`Otel.Baggage.Percent.encode/1` percent-encodes
everything outside `URI.char_unreserved?/1` (`A-Z`, `a-z`,
`0-9`, `-`, `.`, `_`, `~`). That is RFC 3986 strict — but
it over-encodes the token sub-delims. A key like
`user!id` injects as `user%21id`, which a strict W3C parser
reading the wire format may reject because `%21id` is not a
`token`. OTel peers (which decode percent escapes before
comparing) are unaffected — they recover the original
`user!id` and round-trip correctly.

We accept the over-encoding because it gives a single
encode pipeline shared with values (where RFC 3986 is the
right answer per W3C §value L64-L68) and because the
alternative — restricting to RFC 7230 token chars and
rejecting non-token keys — would either silently drop user
baggage or require a separate encoder. Strict W3C
interoperability for non-token keys can be added in a
follow-up; today the trade-off is "over-encoded keys
round-trip with OTel peers, may be rejected by strict
non-OTel parsers".

## Public API

| Function | Role |
|---|---|
| `inject/3` | **SDK** (OTel API MUST) — TextMap Inject (L155-L182); `@impl Otel.Propagator.TextMap` |
| `extract/3` | **SDK** (OTel API MUST) — TextMap Extract (L185-L203); MUST NOT throw on parse failure (L102) |
| `fields/0` | **SDK** (OTel API MUST) — Fields (L133-L152) |
| `encode_baggage/1` | **Application** (W3C header serialization) — §Definition L23-L41 |
| `decode_baggage/1` | **Application** (W3C header parsing) — §Definition L23-L41 |

## References

- W3C Baggage HTTP Header: `w3c-baggage/baggage/HTTP_HEADER_FORMAT.md` L1-L180
- OTel Context §TextMap Propagator: `opentelemetry-specification/specification/context/api-propagators.md` L114-L203
- OTel Context §Extract MUST NOT throw: `opentelemetry-specification/specification/context/api-propagators.md` L100-L102
- Reference impl: `opentelemetry-erlang/apps/opentelemetry_api/src/otel_propagator_baggage.erl`

# `decode_baggage`

```elixir
@spec decode_baggage(header :: String.t()) :: Otel.Baggage.t()
```

**Application** (W3C header parsing) — decodes a `baggage`
header value into an `Otel.Baggage.t()` map.

Splits the header on `,` into `list-member`s per W3C
§Definition L23-L41, delegates each to `decode_entry/1`,
and builds the baggage map. Name and value are RFC 3986
percent-decoded (§value L69); metadata is kept verbatim.

Raises (typically `MatchError`) if any `list-member` is
malformed — for example a pair without `=`. Callers that
need the spec-mandated graceful recovery
(`api-propagators.md` L100-L102 "MUST NOT throw on parse
failure") should go through `extract/3`, which wraps this
call in a `catch` clause.

# `encode_baggage`

```elixir
@spec encode_baggage(baggage :: Otel.Baggage.t()) :: String.t()
```

**Application** (W3C header serialization) — encodes an
`Otel.Baggage.t()` map into a `baggage` header value.

Produces a comma-separated list of `list-member`s per W3C
§Definition L23-L41 (ABNF). Each entry's name and value
are RFC 3986 percent-encoded (§value L64-L68); metadata
is written verbatim (see the module's `## Design notes`
§2 for the opaque-metadata rationale).

Returns `""` for an empty baggage map. The `inject/3`
caller uses that as the signal not to emit the header.

# `extract`

```elixir
@spec extract(
  ctx :: Otel.Ctx.t(),
  carrier :: Otel.Propagator.TextMap.carrier(),
  getter :: Otel.Propagator.TextMap.getter()
) :: Otel.Ctx.t()
```

**SDK** (OTel API MUST) — TextMap "Extract"
(`api-propagators.md` L185-L203) for the W3C `baggage`
header.

Parses the `baggage` header into `{value, metadata}` pairs
and merges the result into `Otel.Baggage.current(ctx)`
(see "Extract merges with existing baggage" in the module
docs).

Per spec L100-L102 **MUST NOT throw on parse failure** —
malformed input (missing `=`, garbage bytes, encoding
errors, etc.) causes the original context to be returned
unchanged via a `catch _, _` clause that covers all three
exit kinds (`:error`, `:throw`, `:exit`) so any abnormal
exit from the parsing pipeline is swallowed. This is an
explicit exception to the project's happy-path policy,
listed under "Not error handling" in
`.claude/rules/code-conventions.md`.

# `fields`

```elixir
@spec fields() :: [String.t()]
```

**SDK** (OTel API MUST) — "Fields" (`api-propagators.md`
L133-L152).

Returns `["baggage"]` — the single header name this
propagator reads and writes.

# `inject`

```elixir
@spec inject(
  ctx :: Otel.Ctx.t(),
  carrier :: Otel.Propagator.TextMap.carrier(),
  setter :: Otel.Propagator.TextMap.setter()
) :: Otel.Propagator.TextMap.carrier()
```

**SDK** (OTel API MUST) — TextMap "Inject"
(`api-propagators.md` L155-L182) for the W3C `baggage`
header.

Serialises `Otel.Baggage.current(ctx)` into a single
comma-separated `baggage` header value and sets it on the
carrier. When the context's baggage is empty the carrier is
returned unchanged (no header written).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
