@zio.dev/zio-blocks 0.0.51 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +6 -0
  4. package/guides/getting-started-with-mux.md +0 -112
  5. package/guides/query-dsl-extending.md +1 -1
  6. package/guides/query-dsl-fluent-builder.md +1 -1
  7. package/guides/query-dsl-reified-optics.md +1 -1
  8. package/guides/query-dsl-sql.md +395 -1
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +131 -70
  12. package/guides/zio-schema-migration.md +6 -6
  13. package/index.md +200 -583
  14. package/package.json +1 -1
  15. package/reference/async.md +1379 -531
  16. package/reference/chunk.md +3 -3
  17. package/reference/codegen/index.md +1 -1
  18. package/reference/combinators.md +4 -4
  19. package/reference/config/config-decoder.md +460 -0
  20. package/reference/config/config-source.md +489 -0
  21. package/reference/config/errors.md +278 -0
  22. package/reference/config/flags.md +369 -0
  23. package/reference/config/formats.md +314 -0
  24. package/reference/config/index.md +304 -0
  25. package/reference/config/rollout.md +336 -0
  26. package/reference/context.md +6 -49
  27. package/reference/data-migration.md +269 -0
  28. package/reference/datastar/attributes.md +302 -0
  29. package/reference/datastar/events.md +234 -0
  30. package/reference/datastar/index.md +256 -0
  31. package/reference/datastar/signals.md +230 -0
  32. package/reference/datastar/sse.md +295 -0
  33. package/reference/datastar.md +2 -2
  34. package/reference/docs.md +2 -2
  35. package/reference/endpoint/bulk-creation.md +96 -0
  36. package/reference/endpoint/index.md +9 -89
  37. package/reference/endpoint/path-codec.md +12 -24
  38. package/reference/endpoint/route-pattern.md +4 -6
  39. package/reference/endpoint/segment-codec.md +19 -32
  40. package/reference/html.md +313 -9
  41. package/reference/htmx/index.md +4 -52
  42. package/reference/htmx/response-headers.md +240 -0
  43. package/reference/http-model/headers.md +735 -0
  44. package/reference/http-model/index.md +3 -1
  45. package/reference/http-model/model.md +107 -71
  46. package/reference/http-model/schema-codecs.md +522 -0
  47. package/reference/http-model/schema.md +6 -3
  48. package/reference/http-model/server-sent-event.md +341 -0
  49. package/reference/jwt.md +195 -0
  50. package/reference/maybe.md +128 -11
  51. package/reference/media-type.md +2 -2
  52. package/reference/mux.md +254 -0
  53. package/reference/mux.mdx +7 -2
  54. package/reference/openapi.md +3 -3
  55. package/reference/projection.md +654 -0
  56. package/reference/resource-management/resource.md +2 -98
  57. package/reference/resource-management/scope.md +1 -209
  58. package/reference/resource-management/wire.md +4 -50
  59. package/reference/ringbuffer/advanced.mdx +1 -1
  60. package/reference/ringbuffer/index.mdx +3 -3
  61. package/reference/ringbuffer/mpmc.mdx +38 -4
  62. package/reference/ringbuffer/mpsc.mdx +36 -4
  63. package/reference/ringbuffer/spmc.mdx +1 -1
  64. package/reference/ringbuffer/spsc.mdx +87 -15
  65. package/reference/schema/allows.md +0 -96
  66. package/reference/schema/binding.md +2 -2
  67. package/reference/schema/built-in-codecs/avro.md +2 -2
  68. package/reference/schema/built-in-codecs/bson.md +50 -20
  69. package/reference/schema/built-in-codecs/csv.md +2 -2
  70. package/reference/schema/built-in-codecs/index.md +3 -3
  71. package/reference/schema/built-in-codecs/json/index.md +2 -2
  72. package/reference/schema/built-in-codecs/messagepack.md +3 -3
  73. package/reference/schema/built-in-codecs/thrift.md +2 -2
  74. package/reference/schema/built-in-codecs/toon.md +3 -3
  75. package/reference/schema/built-in-codecs/yaml.md +2 -2
  76. package/reference/schema/codec.md +11 -11
  77. package/reference/schema/dynamic-optic.md +48 -3
  78. package/reference/schema/dynamic-schema.md +3 -3
  79. package/reference/schema/index.md +2 -0
  80. package/reference/schema/path-interpolator.md +2 -0
  81. package/reference/schema/reflect-transformer.md +140 -0
  82. package/reference/schema/schema-evolution/as.md +4 -4
  83. package/reference/schema/schema-evolution/into.md +2 -2
  84. package/reference/schema/schema-expr.md +2 -2
  85. package/reference/schema/schema-search.md +263 -0
  86. package/reference/schema/schema.md +10 -2
  87. package/reference/schema/type-class-derivation.md +1 -1
  88. package/reference/smithy.md +502 -3
  89. package/reference/sql/db-codec-deriver.md +3 -3
  90. package/reference/sql/db-codec.md +22 -22
  91. package/reference/sql/db-con.md +4 -4
  92. package/reference/sql/db-connection.md +1 -1
  93. package/reference/sql/db-param.md +1 -1
  94. package/reference/sql/db-result-reader.md +4 -2
  95. package/reference/sql/db-tx.md +46 -14
  96. package/reference/sql/ddl.md +1 -1
  97. package/reference/sql/frag.md +44 -10
  98. package/reference/sql/index.md +7 -7
  99. package/reference/sql/repo.md +15 -15
  100. package/reference/sql/sql-dialect.md +1 -1
  101. package/reference/sql/sql-logger.md +1 -1
  102. package/reference/sql/sql-name-mapper.md +3 -3
  103. package/reference/sql/table-metadata.md +3 -3
  104. package/reference/sql/table.md +10 -10
  105. package/reference/sql/transactor-zio.md +1 -1
  106. package/reference/sql/transactor.md +21 -11
  107. package/reference/sql-zio.md +1 -1
  108. package/reference/streams/core/index.md +32 -0
  109. package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
  110. package/reference/streams/{sink.md → core/sink.md} +331 -353
  111. package/reference/streams/{stream.md → core/stream.md} +919 -209
  112. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  113. package/reference/streams/execution-and-compatibility/index.md +35 -0
  114. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  115. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  116. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  117. package/reference/streams/index.md +140 -67
  118. package/reference/streams/primitives/index.md +30 -0
  119. package/reference/streams/primitives/reader.md +1992 -0
  120. package/reference/streams/{writer.md → primitives/writer.md} +254 -98
  121. package/reference/telemetry/common/any-value.md +90 -0
  122. package/reference/telemetry/common/attribute-key.md +87 -0
  123. package/reference/telemetry/common/attributes.md +118 -0
  124. package/reference/telemetry/common/index.md +39 -0
  125. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  126. package/reference/telemetry/common/resource.md +34 -0
  127. package/reference/telemetry/index.md +311 -0
  128. package/reference/telemetry/logging/index.md +197 -0
  129. package/reference/telemetry/logging/log-enrichment.md +72 -0
  130. package/reference/telemetry/logging/log-formatter.md +100 -0
  131. package/reference/telemetry/logging/log-record-processor.md +56 -0
  132. package/reference/telemetry/logging/log-record.md +44 -0
  133. package/reference/telemetry/logging/log-writer.md +64 -0
  134. package/reference/telemetry/logging/logger-provider.md +142 -0
  135. package/reference/telemetry/logging/logger.md +83 -0
  136. package/reference/telemetry/logging/severity.md +62 -0
  137. package/reference/telemetry/metrics/index.md +150 -0
  138. package/reference/telemetry/metrics/instruments.md +183 -0
  139. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  140. package/reference/telemetry/metrics/meter-provider.md +76 -0
  141. package/reference/telemetry/metrics/meter.md +98 -0
  142. package/reference/telemetry/metrics/metric-data.md +57 -0
  143. package/reference/telemetry/otel/custom-exporter.md +216 -0
  144. package/reference/telemetry/otel/index.md +212 -0
  145. package/reference/telemetry/tracing/index.md +155 -0
  146. package/reference/telemetry/tracing/sampler.md +89 -0
  147. package/reference/telemetry/tracing/span-builder.md +57 -0
  148. package/reference/telemetry/tracing/span-context.md +39 -0
  149. package/reference/telemetry/tracing/span-data.md +32 -0
  150. package/reference/telemetry/tracing/span-kind.md +55 -0
  151. package/reference/telemetry/tracing/span-processor.md +53 -0
  152. package/reference/telemetry/tracing/span-status.md +47 -0
  153. package/reference/telemetry/tracing/span.md +117 -0
  154. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  155. package/reference/telemetry/tracing/tracer.md +52 -0
  156. package/reference/typeid.md +0 -64
  157. package/sidebars.js +150 -12
  158. package/undocumented-report.md +528 -270
  159. package/reference/config.md +0 -158
  160. package/reference/streams/concurrent-operators.md +0 -106
  161. package/reference/streams/reader.md +0 -1284
  162. package/reference/streams/scala-2-compatibility.md +0 -55
  163. package/reference/streams/zero-boxing.md +0 -275
  164. package/reference/telemetry.md +0 -693
@@ -0,0 +1,735 @@
1
+ ---
2
+ id: headers
3
+ title: "Header"
4
+ sidebar_label: "Header"
5
+ ---
6
+
7
+ `Header` is the typed model of a single HTTP header field. Each of the 75 built-in headers is a case class or sealed ADT paired with a companion that knows the header's wire name and how to parse and render it. `Header.Codec[A]` is the type class carrying that knowledge, and `Headers` uses it to decode raw strings on demand and cache the result. The lead-in to every typed read:
8
+
9
+ ```scala
10
+ trait Header {
11
+ def headerName: String
12
+ def renderedValue: String
13
+ }
14
+
15
+ object Header {
16
+ trait Codec[A] {
17
+ def name: String
18
+ def parse(value: String): Either[String, A]
19
+ def render(value: A): String
20
+ }
21
+
22
+ trait Typed[H <: Header] extends Codec[H]
23
+
24
+ case class Custom(headerName: String, rawValue: String) extends Header
25
+ }
26
+ ```
27
+
28
+ ## Motivation
29
+
30
+ An HTTP message is a bag of strings, and treating it that way pushes the same three mistakes into every handler. `headers.rawGet("content-length").map(_.toInt)` throws on a malformed value. `headers.rawGet("Content-Length")` works only because someone remembered the casing rules. `cache-control: max-age=600, no-store` has to be split, trimmed, and matched by hand at each call site.
31
+
32
+ A typed header replaces all three with one lookup. `Header.ContentLength` knows its wire name is `content-length`, that the value is a `Long`, and that a non-numeric value is a parse failure rather than an exception. Reading it is `headers.get(Header.ContentLength)`, and the result is an `Option[ContentLength]` with a `length: Long` inside.
33
+
34
+ Parsing stays lazy because most handlers read two or three headers out of fifteen. `Headers` keeps the raw strings and only parses an entry when a typed read asks for it, caching the parsed value per entry so a header read twice is parsed once.
35
+
36
+ ## Quick Showcase
37
+
38
+ Setting a typed header renders it; reading it back parses it:
39
+
40
+ ```scala
41
+ import zio.http.{Header, Headers}
42
+
43
+ val headers = Headers.empty
44
+ .add(Header.ContentLength(1024))
45
+ .add(Header.CacheControl.MaxAge(600))
46
+ .add("x-request-id", "abc-123")
47
+ ```
48
+
49
+ Rendering happened at `Headers#add`, so the collection holds wire-format strings:
50
+
51
+ ```scala
52
+ headers.toList
53
+ // res0: List[Tuple2[String, String]] = List(
54
+ // ("content-length", "1024"),
55
+ // ("cache-control", "max-age=600"),
56
+ // ("x-request-id", "abc-123")
57
+ // )
58
+ ```
59
+
60
+ Typed reads give back the domain value, and untyped reads give back the string:
61
+
62
+ ```scala
63
+ headers.get(Header.ContentLength)
64
+ // res1: Option[ContentLength] = Some(ContentLength(1024L))
65
+ headers.get(Header.CacheControl)
66
+ // res2: Option[CacheControl] = Some(MaxAge(600L))
67
+ headers.rawGet("x-request-id")
68
+ // res3: Option[String] = Some("abc-123")
69
+ ```
70
+
71
+ Lookups are case-insensitive in both directions, because names are lowercased on the way in:
72
+
73
+ ```scala
74
+ headers.rawGet("X-Request-ID")
75
+ // res4: Option[String] = Some("abc-123")
76
+ headers.has("Content-Length")
77
+ // res5: Boolean = true
78
+ ```
79
+
80
+ ## The Codec Type Class
81
+
82
+ `Header.Codec[A]` is what every typed read and write goes through. It pairs a wire name with a parse and a render:
83
+
84
+ ```scala
85
+ trait Codec[A] {
86
+ def name: String
87
+ def parse(value: String): Either[String, A]
88
+ def render(value: A): String
89
+ }
90
+ ```
91
+
92
+ `Header.Typed[H <: Header]` narrows that to codecs whose domain value is itself a `Header`, which is what all 75 built-ins are. Each header's companion object *is* its codec — `Header.ContentLength` refers to the companion when used as a codec and to the case class when used as a type:
93
+
94
+ ```scala
95
+ import zio.http.Header
96
+ ```
97
+
98
+ The codec's name is the canonical lowercase wire name, and parsing reports a message rather than throwing:
99
+
100
+ ```scala
101
+ Header.ContentLength.name
102
+ // res7: String = "content-length"
103
+ Header.ContentLength.parse("1024")
104
+ // res8: Either[String, ContentLength] = Right(ContentLength(1024L))
105
+ Header.ContentLength.parse("not-a-number")
106
+ // res9: Either[String, ContentLength] = Left(
107
+ // "Invalid content-length: not-a-number"
108
+ // )
109
+ ```
110
+
111
+ Rendering is the inverse, and every built-in round-trips:
112
+
113
+ ```scala
114
+ Header.ContentLength.render(Header.ContentLength(1024))
115
+ // res10: String = "1024"
116
+ ```
117
+
118
+ Because `Header` itself carries `headerName` and `renderedValue`, an instance knows how to write itself without the codec being named again:
119
+
120
+ ```scala
121
+ Header.CacheControl.MaxAge(600).headerName
122
+ // res11: String = "cache-control"
123
+ Header.CacheControl.MaxAge(600).renderedValue
124
+ // res12: String = "max-age=600"
125
+ ```
126
+
127
+ ## Reading Headers
128
+
129
+ Six methods read from a `Headers` collection: three typed, three raw. Which you want depends on whether you need the domain value or the exact bytes.
130
+
131
+ ### `Headers#get` — first parseable match
132
+
133
+ `Headers#get` scans for the first entry whose name matches the codec and returns the parsed value:
134
+
135
+ ```scala
136
+ final class Headers {
137
+ def get[A](headerCodec: Header.Codec[A]): Option[A]
138
+ }
139
+ ```
140
+
141
+ A present, well-formed header parses; an absent one is `None`:
142
+
143
+ ```scala
144
+ import zio.http.{Header, Headers}
145
+
146
+ val headers = Headers("content-length" -> "2048", "accept" -> "application/json")
147
+ ```
148
+
149
+ The typed result carries the domain value, not the string:
150
+
151
+ ```scala
152
+ headers.get(Header.ContentLength)
153
+ // res14: Option[ContentLength] = Some(ContentLength(2048L))
154
+ headers.get(Header.ETag)
155
+ // res15: Option[ETag] = None
156
+ ```
157
+
158
+ :::warning[Unparseable headers are skipped, not reported]
159
+ `Headers#get` treats a parse failure exactly like a name mismatch: it discards the error and keeps scanning. A malformed header therefore reads as `None` — or as the *next* entry with the same name that does parse. There is no method that surfaces the parse error from a collection read.
160
+ :::
161
+
162
+ That behaviour is worth seeing, because it is the one place a typed read can mislead you:
163
+
164
+ ```scala
165
+ import zio.http.{Header, Headers}
166
+
167
+ val malformed = Headers("content-length" -> "huge")
168
+ val shadowing = Headers("content-length" -> "huge", "content-length" -> "512")
169
+ ```
170
+
171
+ A single bad value is indistinguishable from an absent header, and a bad value followed by a good one silently yields the good one:
172
+
173
+ ```scala
174
+ malformed.get(Header.ContentLength)
175
+ // res17: Option[ContentLength] = None
176
+ shadowing.get(Header.ContentLength)
177
+ // res18: Option[ContentLength] = Some(ContentLength(512L))
178
+ ```
179
+
180
+ To tell "missing" from "malformed", read the raw value and parse it explicitly:
181
+
182
+ ```scala
183
+ malformed.rawGet("content-length").map(Header.ContentLength.parse)
184
+ // res19: Option[Either[String, ContentLength]] = Some(
185
+ // Left("Invalid content-length: huge")
186
+ // )
187
+ ```
188
+
189
+ ### `Headers#getAll` — every parseable match
190
+
191
+ `Headers#getAll` returns all matching entries in header order, skipping the ones that fail to parse:
192
+
193
+ ```scala
194
+ final class Headers {
195
+ def getAll[A](headerCodec: Header.Codec[A]): Chunk[A]
196
+ }
197
+ ```
198
+
199
+ Multi-value headers are the normal case for `accept-encoding`, `via`, and `set-cookie`:
200
+
201
+ ```scala
202
+ import zio.http.{Header, Headers}
203
+
204
+ val multi = Headers(
205
+ "content-length" -> "100",
206
+ "content-length" -> "oops",
207
+ "content-length" -> "300"
208
+ )
209
+ ```
210
+
211
+ Two entries parse and the middle one is dropped, so the typed result is shorter than the raw one:
212
+
213
+ ```scala
214
+ multi.getAll(Header.ContentLength)
215
+ // res21: Chunk[ContentLength] = IndexedSeq(
216
+ // ContentLength(100L),
217
+ // ContentLength(300L)
218
+ // )
219
+ multi.rawGetAll("content-length").length
220
+ // res22: Int = 3
221
+ ```
222
+
223
+ How many entries survive depends on how strict the individual codec is, and they vary. `Header.ContentLength` rejects anything non-numeric, while some codecs accept almost any input:
224
+
225
+ ```scala
226
+ Header.AcceptEncoding.parse("gzip")
227
+ // res23: Either[String, AcceptEncoding] = Right(GZip(None))
228
+ Header.AcceptEncoding.parse("!!!")
229
+ // res24: Either[String, AcceptEncoding] = Right(GZip(None))
230
+ ```
231
+
232
+ :::warning[`AcceptEncoding` falls back to `GZip` on unknown values]
233
+ `Header.AcceptEncoding` matches a fixed set of encoding names and returns `GZip` for anything it does not recognize, rather than reporting a parse failure. A request with `accept-encoding: bogus` — or a typo like `identityy` — reads as if the client asked for gzip. `Header.AcceptEncoding.parse` rejects only values with no non-empty comma-separated part, so almost any string succeeds. A server choosing a response encoding from this header should read the raw value instead of trusting the parsed variant. Tracked as [zio/zio-blocks#1618](https://github.com/zio/zio-blocks/issues/1618).
234
+ :::
235
+
236
+ ### `Headers#getLast` — last parseable match
237
+
238
+ `Headers#getLast` is `Headers#getAll` keeping only the final element, which is what you want when a later header is meant to override an earlier one:
239
+
240
+ ```scala
241
+ final class Headers {
242
+ def getLast[H <: Header](headerType: Header.Typed[H]): Option[H]
243
+ }
244
+ ```
245
+
246
+ Note the tighter bound: `Headers#getLast` takes a `Header.Typed[H]`, so it works with the built-ins but not with a bare `Header.Codec[A]` over a non-`Header` type:
247
+
248
+ ```scala
249
+ import zio.http.{Header, Headers}
250
+
251
+ val overridden = Headers("content-length" -> "100", "content-length" -> "200")
252
+ ```
253
+
254
+ First and last differ, and each method says which it takes:
255
+
256
+ ```scala
257
+ overridden.get(Header.ContentLength)
258
+ // res26: Option[ContentLength] = Some(ContentLength(100L))
259
+ overridden.getLast(Header.ContentLength)
260
+ // res27: Option[ContentLength] = Some(ContentLength(200L))
261
+ ```
262
+
263
+ ### Raw Access
264
+
265
+ Three methods bypass parsing entirely and hand back the stored string. Use them for headers with no typed model, for pass-through proxying, and for telling a malformed value from an absent one:
266
+
267
+ | Method | Returns | Picks |
268
+ | ------------------------- | ------------------- | ---------------------------- |
269
+ | `Headers#rawGet` | `Option[String]` | First entry with that name |
270
+ | `Headers#rawGetLast` | `Option[String]` | Last entry with that name |
271
+ | `Headers#rawGetAll` | `Chunk[String]` | Every entry, in header order |
272
+
273
+ All three validate the name before scanning and are case-insensitive:
274
+
275
+ ```scala
276
+ import zio.http.Headers
277
+
278
+ val headers = Headers("x-trace" -> "a", "x-trace" -> "b")
279
+ ```
280
+
281
+ Raw reads never fail on content, only on a structurally invalid name:
282
+
283
+ ```scala
284
+ headers.rawGet("X-Trace")
285
+ // res29: Option[String] = Some("a")
286
+ headers.rawGetLast("x-trace")
287
+ // res30: Option[String] = Some("b")
288
+ headers.rawGetAll("x-trace")
289
+ // res31: Chunk[String] = IndexedSeq("a", "b")
290
+ ```
291
+
292
+ `Headers#has` and its alias `Headers#contains` test for presence without reading a value:
293
+
294
+ ```scala
295
+ headers.has("x-trace")
296
+ // res32: Boolean = true
297
+ headers.contains("x-missing")
298
+ // res33: Boolean = false
299
+ ```
300
+
301
+ ## The Parse Cache
302
+
303
+ `Headers` stores three parallel arrays: lowercased names, raw values, and a lazily-filled slot for the parsed value of each entry. A typed read fills that slot; a second read of the same header reuses it.
304
+
305
+ The cache is keyed by *codec identity*, compared by reference. Two codecs sharing a wire name therefore never read each other's cached values, which is what keeps a custom `Header.Codec[MyType]` named `content-length` from colliding with `Header.ContentLength`.
306
+
307
+ Three consequences are worth knowing:
308
+
309
+ - **The cache is dropped by `Headers#add`.** Appending builds fresh arrays and does not carry parsed values across, so a read-then-add-then-read sequence parses twice. Read after you finish assembling, not between additions.
310
+ - **Duplicate parses are possible under concurrency.** Filling a slot is not synchronized, so two threads reading the same header on the same instance may both parse it. Both produce equal values, so the race is benign — but it is a race, and a codec with side effects would run twice.
311
+ - **Failures are not cached.** An entry that fails to parse leaves its slot empty, so every read retries the failing parse.
312
+
313
+ ## Writing Headers
314
+
315
+ Writes come in typed and raw forms, and the distinction that matters is append versus replace.
316
+
317
+ ### Appending and Replacing
318
+
319
+ `Headers#add` appends, keeping any existing header of the same name. `Headers#set` replaces every entry with that name:
320
+
321
+ ```scala
322
+ final class Headers {
323
+ def add(header: Header): Headers
324
+ def add(name: String, value: String): Headers
325
+ def set(header: Header): Headers
326
+ def set(name: String, value: String): Headers
327
+ def remove(name: String): Headers
328
+ }
329
+ ```
330
+
331
+ Starting from a collection that already has a value, the two diverge:
332
+
333
+ ```scala
334
+ import zio.http.{Header, Headers}
335
+
336
+ val base = Headers.empty.add(Header.ContentLength(100))
337
+ ```
338
+
339
+ `Headers#add` produces two entries, while `Headers#set` produces one:
340
+
341
+ ```scala
342
+ base.add(Header.ContentLength(200)).toList
343
+ // res35: List[Tuple2[String, String]] = List(
344
+ // ("content-length", "100"),
345
+ // ("content-length", "200")
346
+ // )
347
+ base.set(Header.ContentLength(200)).toList
348
+ // res36: List[Tuple2[String, String]] = List(("content-length", "200"))
349
+ ```
350
+
351
+ `Headers#remove` drops every entry with the given name, and `Headers#++` concatenates without deduplicating — the result can hold duplicates from both sides:
352
+
353
+ ```scala
354
+ base.remove("content-length").toList
355
+ // res37: List[Tuple2[String, String]] = List()
356
+ (base ++ Headers("content-length" -> "300")).toList
357
+ // res38: List[Tuple2[String, String]] = List(
358
+ // ("content-length", "100"),
359
+ // ("content-length", "300")
360
+ // )
361
+ ```
362
+
363
+ Every method returns a new `Headers`; the class is immutable and the arrays are never mutated in place.
364
+
365
+ ### `Headers.apply` and `Headers.empty`
366
+
367
+ The companion builds a collection from name-value pairs, and `Headers.empty` is the identity for `Headers#++`:
368
+
369
+ ```scala
370
+ object Headers {
371
+ def apply(pairs: (String, String)*): Headers
372
+ val empty: Headers
373
+ }
374
+ ```
375
+
376
+ Pairs are validated and lowercased as they are added:
377
+
378
+ ```scala
379
+ import zio.http.Headers
380
+ ```
381
+
382
+ Both entry points produce the same shape, so a builder chain can start from either:
383
+
384
+ ```scala
385
+ Headers("Content-Type" -> "application/json").toList
386
+ // res40: List[Tuple2[String, String]] = List(
387
+ // ("content-type", "application/json")
388
+ // )
389
+ Headers.empty.size
390
+ // res41: Int = 0
391
+ ```
392
+
393
+ ### `HeadersBuilder` — batch construction
394
+
395
+ Building with `Headers#add` allocates fresh arrays per call, which is wasteful when adding many headers at once. `HeadersBuilder` accumulates into a growable buffer and copies once:
396
+
397
+ ```scala
398
+ final class HeadersBuilder {
399
+ def add(name: String, value: String): Unit
400
+ def reset(): Unit
401
+ def build(): Headers
402
+ }
403
+
404
+ object HeadersBuilder {
405
+ def make(initialCapacity: Int = 8): HeadersBuilder
406
+ }
407
+ ```
408
+
409
+ The builder is mutable and single-threaded by design, and `HeadersBuilder#build` snapshots it:
410
+
411
+ ```scala
412
+ import zio.http.{Header, HeadersBuilder}
413
+
414
+ val builder = HeadersBuilder.make(4)
415
+ builder.add("content-type", "application/json")
416
+ builder.add(Header.ContentLength(64).headerName, Header.ContentLength(64).renderedValue)
417
+ ```
418
+
419
+ The built collection is an ordinary immutable `Headers`:
420
+
421
+ ```scala
422
+ builder.build().toList
423
+ // res45: List[Tuple2[String, String]] = List(
424
+ // ("content-type", "application/json"),
425
+ // ("content-length", "64")
426
+ // )
427
+ ```
428
+
429
+ `HeadersBuilder#reset` clears the buffer for reuse without reallocating, which is why the capacity argument exists. The requested capacity is raised to a minimum of four, and the buffer doubles when it fills.
430
+
431
+ ## Validation and Injection Safety
432
+
433
+ Header names and values are validated on every write, and the value rule exists for a security reason rather than a formatting one.
434
+
435
+ A name must be a non-empty HTTP token: ASCII letters, digits, or one of `!#$%&'*+-.^_`|~`. A value may contain anything **except** carriage return or line feed. Rejecting CR and LF is what prevents response splitting and header injection — a value carrying `\r\n` would otherwise terminate the header and let an attacker append headers or a body of their own choosing.
436
+
437
+ Two public methods expose the checks as values, for validating input before you try to use it:
438
+
439
+ ```scala
440
+ object Headers {
441
+ def validateName(name: String): Either[String, Unit]
442
+ def validateValue(value: String): Either[String, Unit]
443
+ }
444
+ ```
445
+
446
+ Both report a message rather than throwing:
447
+
448
+ ```scala
449
+ import zio.http.Headers
450
+ ```
451
+
452
+ A structurally invalid name and a CR/LF-bearing value are each rejected with a reason:
453
+
454
+ ```scala
455
+ Headers.validateName("x-trace")
456
+ // res47: Either[String, Unit] = Right(())
457
+ Headers.validateName("x trace")
458
+ // res48: Either[String, Unit] = Left("Invalid header name: x trace")
459
+ Headers.validateValue("ok")
460
+ // res49: Either[String, Unit] = Right(())
461
+ Headers.validateValue("evil\r\nSet-Cookie: admin=true")
462
+ // res50: Either[String, Unit] = Left("Header value cannot contain CR or LF")
463
+ ```
464
+
465
+ :::danger[The mutating methods throw]
466
+ `Headers#add`, `Headers#set`, `Headers#remove`, `Headers#has`, the `rawGet*` family, and `HeadersBuilder#add` all enforce the same invariants by throwing `IllegalArgumentException`. When a name or value comes from outside your program — a proxied request, a user-supplied field — check it with `Headers.validateName` or `Headers.validateValue` first, or the throw becomes your error handling.
467
+ :::
468
+
469
+ Note that the raw *read* methods validate their name argument too, so `headers.rawGet(userSuppliedName)` can throw even though it only reads.
470
+
471
+ ## Equality and Rendering
472
+
473
+ `Headers#equals` compares `Headers#toList`, so equality is order-sensitive and name-case-insensitive — names were lowercased on the way in, but two collections holding the same headers in a different order are not equal. `Headers#hashCode` is derived from the same list, so it agrees.
474
+
475
+ `Headers#toString` renders every name and raw value:
476
+
477
+ ```scala
478
+ import zio.http.Headers
479
+
480
+ val withAuth = Headers("authorization" -> "Bearer sk-live-1234567890")
481
+ ```
482
+
483
+ The value appears in full, which matters wherever a `Headers` reaches a log:
484
+
485
+ ```scala
486
+ withAuth.toString
487
+ // res52: String = "Headers(authorization: Bearer sk-live-1234567890)"
488
+ ```
489
+
490
+ :::warning[`toString` prints credentials]
491
+ There is no redaction. `authorization`, `cookie`, and `set-cookie` values are rendered verbatim, and anything that logs a `Request` or `Response` logs its headers. Strip or replace sensitive entries with `Headers#remove` or `Headers#set` before logging.
492
+ :::
493
+
494
+ `Headers#toList` and `Headers#toChunk` expose the same pairs for iteration, differing only in collection type.
495
+
496
+ ## The Header Catalog
497
+
498
+ All 75 built-in headers, grouped the way the module's own test suites group them. Every entry's companion object is its `Header.Typed` codec, and the wire name column is exactly what `Codec#name` returns.
499
+
500
+ ### Authentication
501
+
502
+ | Type | Wire name | Shape |
503
+ | ---------------------------------- | ---------------------- | ---------------------------------------------- |
504
+ | `Header.Authorization` | `authorization` | ADT: `Basic`, `Bearer`, `Digest`, `Unparsed` |
505
+ | `Header.ProxyAuthorization` | `proxy-authorization` | ADT: `Basic`, `Bearer`, `Digest`, `Unparsed` |
506
+ | `Header.WWWAuthenticate` | `www-authenticate` | `scheme: String`, `params: Map[String, String]` |
507
+ | `Header.ProxyAuthenticate` | `proxy-authenticate` | `scheme: String`, `params: Map[String, String]` |
508
+
509
+ `Unparsed` is the escape hatch: a scheme the ADT does not model is preserved rather than rejected, so an unknown authentication scheme survives a parse-render round trip.
510
+
511
+ ### Content
512
+
513
+ | Type | Wire name | Shape |
514
+ | ---------------------------------- | --------------------------- | ---------------------------------------------------------- |
515
+ | `Header.ContentType` | `content-type` | `value: ContentType` |
516
+ | `Header.ContentLength` | `content-length` | `length: Long` |
517
+ | `Header.ContentEncoding` | `content-encoding` | ADT: `GZip`, `Deflate`, `Br`, `Compress`, `Identity`, `Multiple` |
518
+ | `Header.ContentDisposition` | `content-disposition` | ADT: `Attachment`, `Inline`, `FormData` |
519
+ | `Header.ContentLanguage` | `content-language` | `language: String` |
520
+ | `Header.ContentLocation` | `content-location` | `location: String` |
521
+ | `Header.ContentRange` | `content-range` | `unit: String`, `range: Option[(Long, Long)]`, `size: Option[Long]` |
522
+ | `Header.ContentSecurityPolicy` | `content-security-policy` | `directives: String` |
523
+ | `Header.ContentTransferEncoding` | `content-transfer-encoding` | ADT: `SevenBit`, `EightBit`, `Binary`, `QuotedPrintable`, `Base64` |
524
+ | `Header.ContentMd5` | `content-md5` | `value: String` |
525
+ | `Header.ContentBase` | `content-base` | `uri: String` |
526
+
527
+ `Multiple` on `ContentEncoding` carries a `Chunk` of the others, which is how a comma-separated list becomes one value rather than several entries.
528
+
529
+ ### Caching
530
+
531
+ | Type | Wire name | Shape |
532
+ | --------------------------- | --------------------- | ---------------------------------------- |
533
+ | `Header.CacheControl` | `cache-control` | ADT, 17 variants — see below |
534
+ | `Header.ETag` | `etag` | `tag: String`, `weak: Boolean` |
535
+ | `Header.IfMatch` | `if-match` | ADT: `Any`, `ETags` |
536
+ | `Header.IfNoneMatch` | `if-none-match` | ADT: `Any`, `ETags` |
537
+ | `Header.IfModifiedSince` | `if-modified-since` | `date: String` |
538
+ | `Header.IfUnmodifiedSince` | `if-unmodified-since` | `date: String` |
539
+ | `Header.IfRange` | `if-range` | `value: String` |
540
+ | `Header.Expires` | `expires` | `date: String` |
541
+ | `Header.Age` | `age` | `seconds: Long` |
542
+ | `Header.LastModified` | `last-modified` | `date: String` |
543
+ | `Header.Pragma` | `pragma` | `directives: String` |
544
+ | `Header.Vary` | `vary` | ADT: `Any`, `Headers` |
545
+
546
+ `CacheControl` is the largest ADT in the module. Ten variants are flag directives with no argument — `NoCache`, `NoStore`, `NoTransform`, `Public`, `Private`, `MustRevalidate`, `ProxyRevalidate`, `Immutable`, `OnlyIfCached`, `MustUnderstand` — and six take a duration: `MaxAge`, `SMaxAge`, `MinFresh`, `StaleWhileRevalidate`, and `StaleIfError` each hold a `Long`, while `MaxStale` holds an `Option[Long]` because `max-stale` is valid with or without a value. `Multiple` wraps a `Chunk[CacheControl]` for comma-separated directive lists.
547
+
548
+ Parsing splits on `=` to decide between the two families, so an unknown directive name and a non-numeric duration produce different messages:
549
+
550
+ ```scala
551
+ import zio.http.Header
552
+ ```
553
+
554
+ Both failures name what went wrong, which is what makes them useful in a 400 response:
555
+
556
+ ```scala
557
+ Header.CacheControl.parse("max-age=600")
558
+ // res54: Either[String, CacheControl] = Right(MaxAge(600L))
559
+ Header.CacheControl.parse("max-stale")
560
+ // res55: Either[String, CacheControl] = Right(MaxStale(None))
561
+ Header.CacheControl.parse("nonsense")
562
+ // res56: Either[String, CacheControl] = Left(
563
+ // "Unknown cache-control directive: nonsense"
564
+ // )
565
+ Header.CacheControl.parse("max-age=soon")
566
+ // res57: Either[String, CacheControl] = Left(
567
+ // "Invalid number in cache-control directive: soon"
568
+ // )
569
+ ```
570
+
571
+ ### Content Negotiation
572
+
573
+ | Type | Wire name | Shape |
574
+ | ------------------------- | ------------------ | ---------------------------------------------------------------- |
575
+ | `Header.Accept` | `accept` | `mediaRanges: Chunk[Accept.MediaRange]` |
576
+ | `Header.AcceptEncoding` | `accept-encoding` | ADT: `GZip`, `Deflate`, `Br`, `Compress`, `Identity`, `Any`, `Multiple` |
577
+ | `Header.AcceptLanguage` | `accept-language` | `languages: Chunk[AcceptLanguage.LanguageRange]` |
578
+ | `Header.AcceptRanges` | `accept-ranges` | ADT: `Bytes`, `None_` |
579
+ | `Header.AcceptPatch` | `accept-patch` | `mediaTypes: Chunk[MediaType]` |
580
+
581
+ `Accept.MediaRange` and `AcceptLanguage.LanguageRange` are the per-entry types that carry a quality weight, which is what makes these headers a `Chunk` rather than a single value. `None_` on `AcceptRanges` is spelled with a trailing underscore because `None` is taken.
582
+
583
+ ### CORS
584
+
585
+ | Type | Wire name | Shape |
586
+ | ---------------------------------------- | ---------------------------------- | --------------------------------- |
587
+ | `Header.AccessControlAllowOrigin` | `access-control-allow-origin` | ADT: `All`, `Specific` |
588
+ | `Header.AccessControlAllowMethods` | `access-control-allow-methods` | `methods: Chunk[Method]` |
589
+ | `Header.AccessControlAllowHeaders` | `access-control-allow-headers` | `headers: Chunk[String]` |
590
+ | `Header.AccessControlAllowCredentials` | `access-control-allow-credentials` | `allow: Boolean` |
591
+ | `Header.AccessControlExposeHeaders` | `access-control-expose-headers` | `headers: Chunk[String]` |
592
+ | `Header.AccessControlMaxAge` | `access-control-max-age` | `seconds: Long` |
593
+ | `Header.AccessControlRequestHeaders` | `access-control-request-headers` | `headers: Chunk[String]` |
594
+ | `Header.AccessControlRequestMethod` | `access-control-request-method` | `method: Method` |
595
+ | `Header.Origin` | `origin` | ADT: `Null_`, `Value` |
596
+
597
+ `AccessControlAllowMethods` and `AccessControlRequestMethod` hold `Method` values rather than strings, so an unrecognized verb fails at parse time instead of reaching your CORS logic.
598
+
599
+ ### Routing and Identity
600
+
601
+ | Type | Wire name | Shape |
602
+ | ----------------------- | --------------- | ------------------------------------ |
603
+ | `Header.Host` | `host` | `host: String`, `port: Option[Int]` |
604
+ | `Header.Location` | `location` | `uri: String` |
605
+ | `Header.Referer` | `referer` | `uri: String` |
606
+ | `Header.Via` | `via` | `entries: Chunk[String]` |
607
+ | `Header.Forwarded` | `forwarded` | `params: String` |
608
+ | `Header.MaxForwards` | `max-forwards` | `count: Int` |
609
+ | `Header.From` | `from` | `email: String` |
610
+ | `Header.UserAgent` | `user-agent` | `product: String` |
611
+ | `Header.Server` | `server` | `product: String` |
612
+ | `Header.Date` | `date` | `value: String` |
613
+ | `Header.Link` | `link` | `value: String` |
614
+ | `Header.RetryAfter` | `retry-after` | `value: String` |
615
+ | `Header.Allow` | `allow` | `methods: Chunk[Method]` |
616
+ | `Header.Expect` | `expect` | `value: String` |
617
+ | `Header.Range` | `range` | `unit: String`, `ranges: String` |
618
+
619
+ `Host` splits the port out as an `Option[Int]`, so `example.com` and `example.com:8080` both parse and render back to their original form.
620
+
621
+ ### Cookies
622
+
623
+ | Type | Wire name | Shape |
624
+ | -------------------------- | ------------ | --------------- |
625
+ | `Header.CookieHeader` | `cookie` | `value: String` |
626
+ | `Header.SetCookieHeader` | `set-cookie` | `value: String` |
627
+
628
+ Both model the header as an unparsed string; structured cookie handling lives in the separate `Cookie` type documented in [the HTTP model](./model.md). Two aliases exist on the companion for readability at call sites — `Header.Cookie` is `Header.CookieHeader` and `Header.SetCookie` is `Header.SetCookieHeader`, both typed as the codec rather than the case class.
629
+
630
+ ### Connection Management
631
+
632
+ | Type | Wire name | Shape |
633
+ | -------------------------- | -------------------- | ---------------------------------------------------------------------- |
634
+ | `Header.Connection` | `connection` | ADT: `Close`, `KeepAlive`, `Other` |
635
+ | `Header.Upgrade` | `upgrade` | `protocol: String` |
636
+ | `Header.Te` | `te` | `value: String` |
637
+ | `Header.Trailer` | `trailer` | `value: String` |
638
+ | `Header.TransferEncoding` | `transfer-encoding` | ADT: `Chunked`, `Compress`, `Deflate`, `GZip`, `Identity`, `Multiple` |
639
+
640
+ ### Security
641
+
642
+ | Type | Wire name | Shape |
643
+ | ---------------------------------- | ---------------------------- | -------------------------------------------------- |
644
+ | `Header.XFrameOptions` | `x-frame-options` | ADT: `Deny`, `SameOrigin` |
645
+ | `Header.XRequestedWith` | `x-requested-with` | `value: String` |
646
+ | `Header.DNT` | `dnt` | ADT: `TrackingAllowed`, `TrackingNotAllowed`, `Unset` |
647
+ | `Header.UpgradeInsecureRequests` | `upgrade-insecure-requests` | `upgrade: Boolean` |
648
+ | `Header.ClearSiteData` | `clear-site-data` | `directives: Chunk[String]` |
649
+
650
+ ### WebSocket
651
+
652
+ | Type | Wire name | Shape |
653
+ | --------------------------------- | -------------------------- | --------------------------- |
654
+ | `Header.SecWebSocketAccept` | `sec-websocket-accept` | `value: String` |
655
+ | `Header.SecWebSocketExtensions` | `sec-websocket-extensions` | `value: String` |
656
+ | `Header.SecWebSocketKey` | `sec-websocket-key` | `value: String` |
657
+ | `Header.SecWebSocketLocation` | `sec-websocket-location` | `value: String` |
658
+ | `Header.SecWebSocketOrigin` | `sec-websocket-origin` | `value: String` |
659
+ | `Header.SecWebSocketProtocol` | `sec-websocket-protocol` | `protocols: Chunk[String]` |
660
+ | `Header.SecWebSocketVersion` | `sec-websocket-version` | `version: String` |
661
+
662
+ ## Headers Without a Typed Model
663
+
664
+ Two mechanisms cover headers the catalog does not include, and the choice between them is whether you want a value or a type.
665
+
666
+ ### `Header.Custom` — a name and a string
667
+
668
+ `Header.Custom` is a `Header` whose name and value you supply directly. Use it to write an arbitrary header through the same API as a built-in:
669
+
670
+ ```scala
671
+ import zio.http.{Header, Headers}
672
+
673
+ val custom = Header.Custom("x-tenant-id", "acme-42")
674
+ ```
675
+
676
+ It renders exactly what it holds, with no parsing on either side:
677
+
678
+ ```scala
679
+ Headers.empty.add(custom).toList
680
+ // res59: List[Tuple2[String, String]] = List(("x-tenant-id", "acme-42"))
681
+ ```
682
+
683
+ `Header.Custom` has no codec, so it cannot be passed to `Headers#get`. Reading it back means `Headers#rawGet`.
684
+
685
+ ### A Custom `Header.Codec`
686
+
687
+ Implementing `Header.Codec[A]` gives a header both a domain type and typed reads. Nothing requires `A` to extend `Header` unless you also want `Headers#getLast`:
688
+
689
+ ```scala
690
+ import zio.http.{Header, Headers}
691
+
692
+ final case class TenantId(value: String)
693
+
694
+ val tenantIdCodec: Header.Codec[TenantId] = new Header.Codec[TenantId] {
695
+ def name: String = "x-tenant-id"
696
+
697
+ def parse(value: String): Either[String, TenantId] =
698
+ if (value.isEmpty) Left("x-tenant-id must not be empty")
699
+ else Right(TenantId(value))
700
+
701
+ def render(value: TenantId): String = value.value
702
+ }
703
+ ```
704
+
705
+ The custom codec now works with the typed read path, cache included:
706
+
707
+ ```scala
708
+ val headers = Headers("x-tenant-id" -> "acme-42")
709
+ // headers: Headers = Headers(x-tenant-id: acme-42)
710
+ headers.get(tenantIdCodec)
711
+ // res61: Option[TenantId] = Some(TenantId("acme-42"))
712
+ ```
713
+
714
+ A rejected value is skipped like any other parse failure, so the collection read is `None` rather than the error message:
715
+
716
+ ```scala
717
+ Headers("x-tenant-id" -> "").get(tenantIdCodec)
718
+ // res62: Option[TenantId] = None
719
+ tenantIdCodec.parse("")
720
+ // res63: Either[String, TenantId] = Left("x-tenant-id must not be empty")
721
+ ```
722
+
723
+ Return a message describing what was expected. Parse errors reach users through whatever 400 response your handler builds, and the codec's message is the only description available.
724
+
725
+ :::tip[Hold the codec as a `val`]
726
+ The parse cache compares codec identity by reference. A codec constructed inline on every request is a different instance each time, so its cached values are never reused. Define it once as a `val` or an `object`.
727
+ :::
728
+
729
+ ## Integration Points
730
+
731
+ `Header` and `Headers` are used by `Request` and `Response`, which each hold a `Headers` alongside their status or method, URL, and body — see [the HTTP model](./model.md). `Header.ContentType` wraps the `ContentType` type that `Body` also carries, and the media-type values inside it come from `zio-blocks-mediatype`.
732
+
733
+ Several headers hold values from elsewhere in the module rather than strings: `Allow`, `AccessControlAllowMethods`, and `AccessControlRequestMethod` hold `Method`, and `Chunk` is the sequence type throughout.
734
+
735
+ For deriving header codecs from a `Schema[A]` instead of writing them by hand, see [HTTP model schema](./schema.md), which builds `HeaderCodec` and `QueryCodec` instances from schemas. `ServerSentEvent` uses `Headers` indirectly through `Response`, and is documented in [Server-Sent Events](./server-sent-event.md).