@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.
- package/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +6 -0
- package/guides/getting-started-with-mux.md +0 -112
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +395 -1
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +131 -70
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +200 -583
- package/package.json +1 -1
- package/reference/async.md +1379 -531
- package/reference/chunk.md +3 -3
- package/reference/codegen/index.md +1 -1
- package/reference/combinators.md +4 -4
- package/reference/config/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +6 -49
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +2 -2
- package/reference/docs.md +2 -2
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/index.md +9 -89
- package/reference/endpoint/path-codec.md +12 -24
- package/reference/endpoint/route-pattern.md +4 -6
- package/reference/endpoint/segment-codec.md +19 -32
- package/reference/html.md +313 -9
- package/reference/htmx/index.md +4 -52
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +3 -1
- package/reference/http-model/model.md +107 -71
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +6 -3
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +128 -11
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +7 -2
- package/reference/openapi.md +3 -3
- package/reference/projection.md +654 -0
- package/reference/resource-management/resource.md +2 -98
- package/reference/resource-management/scope.md +1 -209
- package/reference/resource-management/wire.md +4 -50
- package/reference/ringbuffer/advanced.mdx +1 -1
- package/reference/ringbuffer/index.mdx +3 -3
- package/reference/ringbuffer/mpmc.mdx +38 -4
- package/reference/ringbuffer/mpsc.mdx +36 -4
- package/reference/ringbuffer/spmc.mdx +1 -1
- package/reference/ringbuffer/spsc.mdx +87 -15
- package/reference/schema/allows.md +0 -96
- package/reference/schema/binding.md +2 -2
- package/reference/schema/built-in-codecs/avro.md +2 -2
- package/reference/schema/built-in-codecs/bson.md +50 -20
- package/reference/schema/built-in-codecs/csv.md +2 -2
- package/reference/schema/built-in-codecs/index.md +3 -3
- package/reference/schema/built-in-codecs/json/index.md +2 -2
- package/reference/schema/built-in-codecs/messagepack.md +3 -3
- package/reference/schema/built-in-codecs/thrift.md +2 -2
- package/reference/schema/built-in-codecs/toon.md +3 -3
- package/reference/schema/built-in-codecs/yaml.md +2 -2
- package/reference/schema/codec.md +11 -11
- package/reference/schema/dynamic-optic.md +48 -3
- package/reference/schema/dynamic-schema.md +3 -3
- package/reference/schema/index.md +2 -0
- package/reference/schema/path-interpolator.md +2 -0
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/schema/schema-evolution/as.md +4 -4
- package/reference/schema/schema-evolution/into.md +2 -2
- package/reference/schema/schema-expr.md +2 -2
- package/reference/schema/schema-search.md +263 -0
- package/reference/schema/schema.md +10 -2
- package/reference/schema/type-class-derivation.md +1 -1
- package/reference/smithy.md +502 -3
- package/reference/sql/db-codec-deriver.md +3 -3
- package/reference/sql/db-codec.md +22 -22
- package/reference/sql/db-con.md +4 -4
- package/reference/sql/db-connection.md +1 -1
- package/reference/sql/db-param.md +1 -1
- package/reference/sql/db-result-reader.md +4 -2
- package/reference/sql/db-tx.md +46 -14
- package/reference/sql/ddl.md +1 -1
- package/reference/sql/frag.md +44 -10
- package/reference/sql/index.md +7 -7
- package/reference/sql/repo.md +15 -15
- package/reference/sql/sql-dialect.md +1 -1
- package/reference/sql/sql-logger.md +1 -1
- package/reference/sql/sql-name-mapper.md +3 -3
- package/reference/sql/table-metadata.md +3 -3
- package/reference/sql/table.md +10 -10
- package/reference/sql/transactor-zio.md +1 -1
- package/reference/sql/transactor.md +21 -11
- package/reference/sql-zio.md +1 -1
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
- package/reference/streams/{sink.md → core/sink.md} +331 -353
- package/reference/streams/{stream.md → core/stream.md} +919 -209
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +140 -67
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/{writer.md → primitives/writer.md} +254 -98
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +0 -64
- package/sidebars.js +150 -12
- package/undocumented-report.md +528 -270
- package/reference/config.md +0 -158
- package/reference/streams/concurrent-operators.md +0 -106
- package/reference/streams/reader.md +0 -1284
- package/reference/streams/scala-2-compatibility.md +0 -55
- package/reference/streams/zero-boxing.md +0 -275
- 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).
|