@zio.dev/zio-blocks 0.0.28 → 0.0.29

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.
@@ -0,0 +1,1716 @@
1
+ ---
2
+ id: http-model
3
+ title: "HTTP Model"
4
+ ---
5
+
6
+ `zio-http-model` is a **pure, zero-dependency HTTP data model** for building HTTP clients and servers. It provides immutable types representing requests, responses, headers, URLs, paths, query parameters, and all HTTP primitives.
7
+
8
+ The module is designed as a pure data layer:
9
+
10
+ - **Zero effects**: no streaming, no I/O, no mutable state (except monotonic lazy-parse caches in `Headers`)
11
+ - **Zero ZIO dependency**: uses `zio.blocks.chunk.Chunk`, not `zio.Chunk`
12
+ - **Single encoding contract**: `Path` and `QueryParams` store values decoded internally, encode only on output
13
+ - **Cross-platform**: JVM and Scala.js support
14
+ - **Cross-version**: Scala 2.13 and 3.x support
15
+
16
+ ```scala
17
+ package zio.http
18
+
19
+ // Core request/response types
20
+ final case class Request(method: Method, url: URL, headers: Headers, body: Body, version: Version)
21
+ final case class Response(status: Status, headers: Headers, body: Body, version: Version)
22
+
23
+ // URL structure
24
+ final case class URL(scheme: Option[Scheme], host: Option[String], port: Option[Int],
25
+ path: Path, queryParams: QueryParams, fragment: Option[String])
26
+
27
+ final case class Path(segments: Chunk[String], hasLeadingSlash: Boolean, trailingSlash: Boolean)
28
+ final class QueryParams private[http] (...)
29
+
30
+ // HTTP primitives
31
+ sealed abstract class Method(val name: String, val ordinal: Int)
32
+ opaque type Status = Int // Scala 3
33
+ sealed abstract class Version(val major: Int, val minor: Int)
34
+ sealed trait Scheme
35
+
36
+ // Headers and body
37
+ final class Headers private[http] (...)
38
+ sealed trait Header
39
+ final class Body private (val data: Chunk[Byte], val contentType: ContentType)
40
+
41
+ // Supporting types
42
+ final case class ContentType(mediaType: MediaType, boundary: Option[Boundary], charset: Option[Charset])
43
+ final case class ResponseCookie(...), RequestCookie(name: String, value: String)
44
+ final case class Form(entries: Chunk[(String, String)])
45
+ ```
46
+
47
+ ## Motivation
48
+
49
+ HTTP libraries often couple protocol concerns with effects and streaming, making it difficult to:
50
+
51
+ - Share data structures across client and server implementations
52
+ - Serialize requests/responses for caching or testing
53
+ - Work with HTTP primitives without committing to a specific effect system
54
+
55
+ `zio-http-model` solves this by providing pure data types that:
56
+
57
+ - Represent all HTTP concepts as immutable values
58
+ - Encode only on output (decode on input, store decoded)
59
+ - Parse incrementally with lazy caching (`Headers` parses typed headers on first access and caches the result)
60
+ - Work with any effect system or none at all
61
+
62
+ ## Installation
63
+
64
+ Add the following to your `build.sbt`:
65
+
66
+ ```scala
67
+ libraryDependencies += "dev.zio" %% "zio-http-model" % "<version>"
68
+ ```
69
+
70
+ For cross-platform projects (Scala.js):
71
+
72
+ ```scala
73
+ libraryDependencies += "dev.zio" %%% "zio-http-model" % "<version>"
74
+ ```
75
+
76
+ Supported Scala versions: 2.13.x and 3.x
77
+
78
+ ## Quick Start
79
+
80
+ Creating a request with query parameters:
81
+
82
+ ```scala
83
+ import zio.http._
84
+
85
+ val url = URL.parse("https://api.example.com/users?active=true").toOption.get
86
+ val request = Request.get(url)
87
+
88
+ val withHeader = request.addHeader("authorization", "Bearer token123")
89
+ ```
90
+
91
+ Creating a JSON response:
92
+
93
+ ```scala
94
+ import zio.http._
95
+
96
+ val jsonBody = Body.fromString("""{"message":"ok"}""", Charset.UTF8)
97
+ val response = Response(
98
+ status = Status.Ok,
99
+ body = jsonBody,
100
+ headers = Headers("content-type" -> "application/json")
101
+ )
102
+ ```
103
+
104
+ ## Method
105
+
106
+ `Method` represents standard HTTP methods as case objects:
107
+
108
+ ```scala
109
+ sealed abstract class Method(val name: String, val ordinal: Int)
110
+ ```
111
+
112
+ ### Predefined Methods
113
+
114
+ ```scala
115
+ import zio.http.Method
116
+
117
+ val get = Method.GET
118
+ val post = Method.POST
119
+ val put = Method.PUT
120
+ val delete = Method.DELETE
121
+ val patch = Method.PATCH
122
+ val head = Method.HEAD
123
+ val options = Method.OPTIONS
124
+ val trace = Method.TRACE
125
+ val connect = Method.CONNECT
126
+ ```
127
+
128
+ ### Parsing
129
+
130
+ ```scala
131
+ import zio.http.Method
132
+
133
+ Method.fromString("GET") // Some(Method.GET)
134
+ Method.fromString("POST") // Some(Method.POST)
135
+ Method.fromString("CUSTOM") // None (unknown method)
136
+ ```
137
+
138
+ ### Rendering
139
+
140
+ ```scala
141
+ import zio.http.Method
142
+
143
+ Method.render(Method.GET) // "GET"
144
+ Method.GET.name // "GET"
145
+ Method.GET.toString // "GET"
146
+ ```
147
+
148
+ ## Status
149
+
150
+ `Status` is an opaque type alias for `Int` in Scala 3 (AnyVal wrapper in Scala 2.13), providing zero-allocation status codes with predefined constants.
151
+
152
+ ```scala
153
+ opaque type Status = Int // Scala 3
154
+ ```
155
+
156
+ ### Predefined Status Codes
157
+
158
+ Status codes are organized by category:
159
+
160
+ ```scala
161
+ import zio.http.Status
162
+
163
+ // 1xx Informational
164
+ Status.Continue // 100
165
+ Status.SwitchingProtocols // 101
166
+
167
+ // 2xx Success
168
+ Status.Ok // 200
169
+ Status.Created // 201
170
+ Status.NoContent // 204
171
+
172
+ // 3xx Redirection
173
+ Status.MovedPermanently // 301
174
+ Status.Found // 302
175
+ Status.SeeOther // 303
176
+ Status.NotModified // 304
177
+
178
+ // 4xx Client Errors
179
+ Status.BadRequest // 400
180
+ Status.Unauthorized // 401
181
+ Status.Forbidden // 403
182
+ Status.NotFound // 404
183
+
184
+ // 5xx Server Errors
185
+ Status.InternalServerError // 500
186
+ Status.BadGateway // 502
187
+ Status.ServiceUnavailable // 503
188
+ ```
189
+
190
+ ### Creating Status Codes
191
+
192
+ ```scala
193
+ import zio.http.Status
194
+
195
+ val custom = Status(418) // I'm a teapot
196
+ val ok = Status.fromInt(200) // Status.Ok
197
+ ```
198
+
199
+ ### Status Code Operations
200
+
201
+ ```scala
202
+ import zio.http.Status
203
+
204
+ val status = Status.Ok
205
+
206
+ status.code // 200
207
+ status.text // "OK"
208
+ status.isSuccess // true (2xx)
209
+ status.isInformational // false (1xx)
210
+ status.isRedirection // false (3xx)
211
+ status.isClientError // false (4xx)
212
+ status.isServerError // false (5xx)
213
+ status.isError // false (4xx or 5xx)
214
+ ```
215
+
216
+ ## Version
217
+
218
+ `Version` represents HTTP protocol versions:
219
+
220
+ ```scala
221
+ sealed abstract class Version(val major: Int, val minor: Int)
222
+ ```
223
+
224
+ ### Predefined Versions
225
+
226
+ ```scala
227
+ import zio.http.Version
228
+
229
+ val v10 = Version.`HTTP/1.0`
230
+ val v11 = Version.`HTTP/1.1`
231
+ val v20 = Version.`HTTP/2.0`
232
+ val v30 = Version.`HTTP/3.0`
233
+ ```
234
+
235
+ ### Parsing and Rendering
236
+
237
+ ```scala
238
+ import zio.http.Version
239
+
240
+ Version.fromString("HTTP/1.1") // Some(Version.`HTTP/1.1`)
241
+ Version.fromString("HTTP/2") // Some(Version.`HTTP/2.0`)
242
+ Version.fromString("HTTP/3") // Some(Version.`HTTP/3.0`)
243
+
244
+ Version.render(Version.`HTTP/1.1`) // "HTTP/1.1"
245
+ Version.`HTTP/2.0`.text // "HTTP/2.0"
246
+ ```
247
+
248
+ ## Scheme
249
+
250
+ `Scheme` represents URL schemes with support for HTTP, HTTPS, WebSocket (WS, WSS), and custom schemes:
251
+
252
+ ```scala
253
+ sealed trait Scheme {
254
+ def text: String
255
+ def defaultPort: Option[Int]
256
+ def isSecure: Boolean
257
+ def isWebSocket: Boolean
258
+ }
259
+ ```
260
+
261
+ ### Predefined Schemes
262
+
263
+ ```scala
264
+ import zio.http.Scheme
265
+
266
+ val http = Scheme.HTTP // http://, port 80
267
+ val https = Scheme.HTTPS // https://, port 443
268
+ val ws = Scheme.WS // ws://, port 80
269
+ val wss = Scheme.WSS // wss://, port 443
270
+ ```
271
+
272
+ ### Scheme Properties
273
+
274
+ ```scala
275
+ import zio.http.Scheme
276
+
277
+ Scheme.HTTPS.isSecure // true
278
+ Scheme.WS.isWebSocket // true
279
+ Scheme.HTTP.defaultPort // Some(80)
280
+ ```
281
+
282
+ ### Custom Schemes
283
+
284
+ ```scala
285
+ import zio.http.Scheme
286
+
287
+ val custom = Scheme.Custom("git+ssh")
288
+ custom.text // "git+ssh"
289
+ custom.defaultPort // None
290
+ ```
291
+
292
+ ### Parsing
293
+
294
+ ```scala
295
+ import zio.http.Scheme
296
+
297
+ Scheme.fromString("https") // Scheme.HTTPS
298
+ Scheme.fromString("wss") // Scheme.WSS
299
+ Scheme.fromString("custom") // Scheme.Custom("custom")
300
+ ```
301
+
302
+ ## Charset
303
+
304
+ `Charset` represents character encodings with JVM-only conversion to `java.nio.charset.Charset`:
305
+
306
+ ```scala
307
+ sealed abstract class Charset(val name: String)
308
+ ```
309
+
310
+ ### Predefined Charsets
311
+
312
+ ```scala
313
+ import zio.http.Charset
314
+
315
+ val utf8 = Charset.UTF8 // "UTF-8"
316
+ val ascii = Charset.ASCII // "US-ASCII"
317
+ val iso88591 = Charset.ISO_8859_1 // "ISO-8859-1"
318
+ val utf16 = Charset.UTF16 // "UTF-16"
319
+ val utf16be = Charset.UTF16BE // "UTF-16BE"
320
+ val utf16le = Charset.UTF16LE // "UTF-16LE"
321
+ ```
322
+
323
+ ### Parsing
324
+
325
+ ```scala
326
+ import zio.http.Charset
327
+
328
+ Charset.fromString("UTF-8") // Some(Charset.UTF8)
329
+ Charset.fromString("utf8") // Some(Charset.UTF8) (case-insensitive)
330
+ Charset.fromString("ISO-8859-1") // Some(Charset.ISO_8859_1)
331
+ Charset.fromString("LATIN1") // Some(Charset.ISO_8859_1) (alias)
332
+ ```
333
+
334
+ ## Boundary
335
+
336
+ `Boundary` represents multipart form-data boundaries:
337
+
338
+ ```scala
339
+ import zio.http.Boundary
340
+
341
+ val boundary = Boundary("----WebKitFormBoundary7MA4YWxkTrZu0gW")
342
+ boundary.value // "----WebKitFormBoundary7MA4YWxkTrZu0gW"
343
+ boundary.toString // "----WebKitFormBoundary7MA4YWxkTrZu0gW"
344
+ ```
345
+
346
+ ### Generating Boundaries
347
+
348
+ ```scala
349
+ import zio.http.Boundary
350
+
351
+ val generated = Boundary.generate // random 24-character alphanumeric string
352
+ ```
353
+
354
+ ## PercentEncoder
355
+
356
+ `PercentEncoder` provides RFC 3986 percent-encoding for URL components. Each URL component has different encoding rules:
357
+
358
+ ```scala
359
+ import zio.http.PercentEncoder
360
+ import zio.http.PercentEncoder.ComponentType
361
+
362
+ val segment = PercentEncoder.encode("hello world", ComponentType.PathSegment)
363
+ // "hello%20world"
364
+
365
+ val queryKey = PercentEncoder.encode("filter[name]", ComponentType.QueryKey)
366
+ // "filter%5Bname%5D"
367
+
368
+ val decoded = PercentEncoder.decode("hello%20world")
369
+ // "hello world"
370
+ ```
371
+
372
+ ### Component Types
373
+
374
+ The encoder recognizes these component types:
375
+
376
+ - `PathSegment`: path segments between `/`
377
+ - `QueryKey`: query parameter names
378
+ - `QueryValue`: query parameter values
379
+ - `Fragment`: fragment identifiers after `#`
380
+ - `UserInfo`: userinfo in authority
381
+
382
+ Each type has specific rules for which characters must be percent-encoded.
383
+
384
+ ## ContentType
385
+
386
+ `ContentType` combines a media type with optional charset and boundary parameters:
387
+
388
+ ```scala
389
+ import zio.http.{ContentType, Charset, Boundary}
390
+ import zio.blocks.mediatype.MediaTypes
391
+
392
+ val json = ContentType(MediaTypes.application.`json`)
393
+
394
+ val htmlUtf8 = ContentType(
395
+ mediaType = MediaTypes.text.`html`,
396
+ charset = Some(Charset.UTF8)
397
+ )
398
+
399
+ val multipart = ContentType(
400
+ mediaType = MediaTypes.multipart.`form-data`,
401
+ boundary = Some(Boundary("----boundary"))
402
+ )
403
+ ```
404
+
405
+ ### Parsing
406
+
407
+ ```scala
408
+ import zio.http.ContentType
409
+
410
+ ContentType.parse("application/json")
411
+ // Right(ContentType(MediaType("application", "json")))
412
+
413
+ ContentType.parse("text/html; charset=utf-8")
414
+ // Right(ContentType(..., charset = Some(Charset.UTF8)))
415
+
416
+ ContentType.parse("multipart/form-data; boundary=abc123")
417
+ // Right(ContentType(..., boundary = Some(Boundary("abc123"))))
418
+
419
+ ContentType.parse("")
420
+ // Left("Invalid content type: cannot be empty")
421
+ ```
422
+
423
+ ### Rendering
424
+
425
+ ```scala
426
+ import zio.http.{ContentType, Charset}
427
+ import zio.blocks.mediatype.MediaTypes
428
+
429
+ val ct = ContentType(
430
+ MediaTypes.text.`plain`,
431
+ charset = Some(Charset.UTF8)
432
+ )
433
+
434
+ ct.render // "text/plain; charset=UTF-8"
435
+ ```
436
+
437
+ ### Predefined Content Types
438
+
439
+ ```scala
440
+ import zio.http.ContentType
441
+
442
+ val json = ContentType.`application/json`
443
+ val plain = ContentType.`text/plain`
444
+ val html = ContentType.`text/html`
445
+ val binary = ContentType.`application/octet-stream`
446
+ ```
447
+
448
+ ## Path
449
+
450
+ `Path` represents URL paths with decoded segments stored internally:
451
+
452
+ ```scala
453
+ final case class Path(
454
+ segments: Chunk[String],
455
+ hasLeadingSlash: Boolean,
456
+ trailingSlash: Boolean
457
+ )
458
+ ```
459
+
460
+ Paths use a **single encoding contract**: decode on input, store decoded, encode on output.
461
+
462
+ ### Creating Paths
463
+
464
+ ```scala
465
+ import zio.http.Path
466
+
467
+ val empty = Path.empty // ""
468
+ val root = Path.root // "/"
469
+ val users = Path("/users") // segments: ["users"], leading slash: true
470
+ val api = Path("api/v1/users/") // segments: ["api", "v1", "users"], trailing slash: true
471
+ ```
472
+
473
+ ### Parsing Encoded Paths
474
+
475
+ `Path.fromEncoded` decodes percent-encoded segments:
476
+
477
+ ```scala
478
+ import zio.http.Path
479
+
480
+ val path = Path.fromEncoded("/hello%20world/foo%2Fbar")
481
+ // Path(Chunk("hello world", "foo/bar"), hasLeadingSlash = true, trailingSlash = false)
482
+
483
+ path.segments(0) // "hello world" (decoded)
484
+ path.segments(1) // "foo/bar" (decoded)
485
+ ```
486
+
487
+ ### Building Paths
488
+
489
+ ```scala
490
+ import zio.http.Path
491
+
492
+ val base = Path("/api")
493
+ val extended = base / "users" / "123" // "/api/users/123"
494
+
495
+ val combined = Path("/api") ++ Path("v1/users") // "/api/v1/users"
496
+ ```
497
+
498
+ ### Encoding Paths
499
+
500
+ ```scala
501
+ import zio.http.Path
502
+ import zio.blocks.chunk.Chunk
503
+
504
+ val path = Path(Chunk("hello world", "foo/bar"), hasLeadingSlash = true, trailingSlash = false)
505
+
506
+ path.encode // "/hello%20world/foo%2Fbar" (percent-encoded)
507
+ path.render // "/hello world/foo/bar" (decoded for display)
508
+ ```
509
+
510
+ ### Path Properties
511
+
512
+ ```scala
513
+ import zio.http.Path
514
+
515
+ val path = Path("/api/v1/users/")
516
+
517
+ path.isEmpty // false
518
+ path.nonEmpty // true
519
+ path.length // 3 (number of segments)
520
+ path.hasLeadingSlash // true
521
+ path.trailingSlash // true
522
+ ```
523
+
524
+ ### Path Navigation
525
+
526
+ ```scala
527
+ import zio.http.Path
528
+
529
+ val path = Path("/api/v1/users")
530
+
531
+ // Slash manipulation
532
+ path.addLeadingSlash // same (already has one)
533
+ path.dropLeadingSlash // "api/v1/users" (relative)
534
+ path.addTrailingSlash // "/api/v1/users/"
535
+ path.dropTrailingSlash // same (already no trailing slash)
536
+
537
+ // Inspecting
538
+ path.isRoot // false (root is "/" with no segments)
539
+ Path.root.isRoot // true
540
+
541
+ // Prefix checking
542
+ path.startsWith(Path("api/v1")) // true
543
+
544
+ // Slicing
545
+ path.drop(1) // "/v1/users" (drop first segment)
546
+ path.take(2) // "/api/v1" (take first 2 segments)
547
+ path.dropRight(1) // "/api/v1" (drop last segment)
548
+ path.initial // "/api/v1" (all but last)
549
+ path.last // Some("users")
550
+ path.reverse // "/users/v1/api"
551
+ ```
552
+
553
+ ## QueryParams
554
+
555
+ `QueryParams` stores query parameters with multiple values per key:
556
+
557
+ ```scala
558
+ final class QueryParams private[http] (
559
+ private val keys: Array[String],
560
+ private val vals: Array[Chunk[String]],
561
+ val size: Int
562
+ )
563
+ ```
564
+
565
+ Like `Path`, query parameters store decoded values internally and encode only on output.
566
+
567
+ ### Creating QueryParams
568
+
569
+ ```scala
570
+ import zio.http.QueryParams
571
+
572
+ val empty = QueryParams.empty
573
+
574
+ val params = QueryParams(
575
+ "name" -> "Alice",
576
+ "age" -> "30",
577
+ "active" -> "true"
578
+ )
579
+ ```
580
+
581
+ ### Parsing Encoded Query Strings
582
+
583
+ ```scala
584
+ import zio.http.QueryParams
585
+
586
+ val params = QueryParams.fromEncoded("name=Alice%20Smith&age=30&active=true")
587
+
588
+ params.getFirst("name") // Some("Alice Smith") (decoded)
589
+ params.getFirst("age") // Some("30")
590
+ params.getFirst("active") // Some("true")
591
+ ```
592
+
593
+ ### Accessing Values
594
+
595
+ ```scala
596
+ import zio.http.QueryParams
597
+
598
+ val params = QueryParams(
599
+ "color" -> "red",
600
+ "color" -> "blue",
601
+ "size" -> "large"
602
+ )
603
+
604
+ params.get("color") // Some(Chunk("red", "blue"))
605
+ params.getFirst("color") // Some("red")
606
+ params.getFirst("size") // Some("large")
607
+ params.getFirst("other") // None
608
+ params.has("color") // true
609
+ ```
610
+
611
+ ### Modifying QueryParams
612
+
613
+ ```scala
614
+ import zio.http.QueryParams
615
+
616
+ val params = QueryParams("a" -> "1", "b" -> "2")
617
+
618
+ val added = params.add("c", "3") // adds "c=3"
619
+ val set = params.set("a", "100") // replaces all "a" values
620
+ val removed = params.remove("b") // removes all "b" entries
621
+ ```
622
+
623
+ ### Encoding
624
+
625
+ ```scala
626
+ import zio.http.QueryParams
627
+
628
+ val params = QueryParams(
629
+ "name" -> "Alice Smith",
630
+ "filter[status]" -> "active"
631
+ )
632
+
633
+ params.encode // "name=Alice%20Smith&filter%5Bstatus%5D=active"
634
+ ```
635
+
636
+ ### Converting to List
637
+
638
+ ```scala
639
+ import zio.http.QueryParams
640
+
641
+ val params = QueryParams("a" -> "1", "a" -> "2", "b" -> "3")
642
+ params.toList // List(("a", "1"), ("a", "2"), ("b", "3"))
643
+ ```
644
+
645
+ ## URL
646
+
647
+ `URL` combines scheme, host, port, path, query parameters, and fragment:
648
+
649
+ ```scala
650
+ final case class URL(
651
+ scheme: Option[Scheme],
652
+ host: Option[String],
653
+ port: Option[Int],
654
+ path: Path,
655
+ queryParams: QueryParams,
656
+ fragment: Option[String]
657
+ )
658
+ ```
659
+
660
+ ### Parsing URLs
661
+
662
+ ```scala
663
+ import zio.http.URL
664
+
665
+ val absolute = URL.parse("https://api.example.com:8080/users?active=true#results")
666
+ // Right(URL(
667
+ // scheme = Some(Scheme.HTTPS),
668
+ // host = Some("api.example.com"),
669
+ // port = Some(8080),
670
+ // path = Path("/users"),
671
+ // queryParams = QueryParams("active" -> "true"),
672
+ // fragment = Some("results")
673
+ // ))
674
+
675
+ val relative = URL.parse("/api/users?page=2")
676
+ // Right(URL(
677
+ // scheme = None,
678
+ // host = None,
679
+ // port = None,
680
+ // path = Path("/api/users"),
681
+ // queryParams = QueryParams("page" -> "2"),
682
+ // fragment = None
683
+ // ))
684
+ ```
685
+
686
+ The parser handles:
687
+
688
+ - IPv6 hosts in brackets: `http://[::1]:8080/`
689
+ - Userinfo: `https://user:pass@example.com/` (userinfo is skipped)
690
+ - Relative URLs: `/path?query`
691
+ - Fragment identifiers: `#section`
692
+
693
+ ### Building URLs
694
+
695
+ ```scala
696
+ import zio.http.{URL, Path, Scheme}
697
+
698
+ val base = URL.root // http://localhost/
699
+
700
+ val extended = base / "api" / "users" // adds path segments
701
+
702
+ val withQuery = extended ?? ("active", "true") ?? ("page", "1")
703
+ // http://localhost/api/users?active=true&page=1
704
+ ```
705
+
706
+ ### URL from Path
707
+
708
+ ```scala
709
+ import zio.http.{URL, Path}
710
+
711
+ val path = Path("/api/users")
712
+ val url = URL.fromPath(path) // relative URL with just path
713
+ ```
714
+
715
+ ### Encoding URLs
716
+
717
+ ```scala
718
+ import zio.http.URL
719
+
720
+ val url = URL.parse("https://example.com/hello world?name=Alice Smith").toOption.get
721
+
722
+ url.encode // "https://example.com/hello%20world?name=Alice%20Smith"
723
+ url.toString // same as encode
724
+ ```
725
+
726
+ ### URL Properties
727
+
728
+ ```scala
729
+ import zio.http.URL
730
+
731
+ val absolute = URL.parse("https://example.com/").toOption.get
732
+ val relative = URL.parse("/api/users").toOption.get
733
+
734
+ absolute.isAbsolute // true (has scheme)
735
+ relative.isRelative // true (no scheme)
736
+ ```
737
+
738
+ ### URL Transformation
739
+
740
+ ```scala
741
+ import zio.http.{URL, Path, Scheme, QueryParams}
742
+
743
+ val url = URL.parse("https://api.example.com:8080/users?page=1").toOption.get
744
+
745
+ // Setting components
746
+ url.host("other.com") // changes host
747
+ url.port(9090) // changes port
748
+ url.scheme(Scheme.HTTP) // changes scheme
749
+ url.path(Path("/v2/users")) // replaces path
750
+ url.fragment("top") // sets fragment
751
+
752
+ // Adding paths
753
+ url.addPath("123") // appends segment: /users/123
754
+ url.addPath(Path("v2/items")) // appends multi-segment path
755
+
756
+ // Query manipulation
757
+ url.addQueryParams(QueryParams("sort" -> "name"))
758
+ url.updateQueryParams(_.remove("page"))
759
+
760
+ // Slash manipulation (delegates to path)
761
+ url.addLeadingSlash
762
+ url.dropTrailingSlash
763
+
764
+ // Derived properties
765
+ url.hostPort // Some("api.example.com:8080")
766
+ url.relative // strips scheme/host/port, keeps path and query
767
+ ```
768
+
769
+ ## Header
770
+
771
+ `Header` is a trait representing typed HTTP headers:
772
+
773
+ ```scala
774
+ trait Header {
775
+ def headerName: String
776
+ def renderedValue: String
777
+ }
778
+ ```
779
+
780
+ Each header type has a companion object implementing `Header.Typed[H]` for parsing and rendering.
781
+
782
+ ### Predefined Header Types
783
+
784
+ ```scala
785
+ import zio.http.{Header => _, *}
786
+ import zio.http.headers
787
+
788
+ val contentType = headers.ContentType
789
+ val accept = headers.Accept
790
+ val authorization = headers.Authorization
791
+ val host = headers.Host
792
+ val userAgent = headers.UserAgent
793
+ val cacheControl = headers.CacheControl
794
+ val contentLength = headers.ContentLength
795
+ val location = headers.Location
796
+ val setCookie = headers.SetCookieHeader
797
+ val cookie = headers.CookieHeader
798
+ ```
799
+
800
+ ### Creating Typed Headers
801
+
802
+ ```scala
803
+ import zio.http.{Header => _, ContentType, Charset, *}
804
+ import zio.http.headers
805
+ import zio.blocks.mediatype.MediaTypes
806
+
807
+ val ct = headers.ContentType(
808
+ ContentType(MediaTypes.application.`json`, charset = Some(Charset.UTF8))
809
+ )
810
+
811
+ val auth = headers.Authorization.Bearer("token123")
812
+
813
+ val host = headers.Host("api.example.com", Some(8080))
814
+ ```
815
+
816
+ ### Parsing Headers
817
+
818
+ ```scala
819
+ import zio.http.{Header => _, *}
820
+ import zio.http.headers
821
+
822
+ headers.ContentType.parse("application/json; charset=utf-8")
823
+ // Right(headers.ContentType(...))
824
+
825
+ headers.Host.parse("example.com:443")
826
+ // Right(headers.Host("example.com", Some(443)))
827
+
828
+ headers.ContentLength.parse("1024")
829
+ // Right(headers.ContentLength(1024))
830
+
831
+ headers.ContentLength.parse("-1")
832
+ // Left("Invalid content-length: -1")
833
+ ```
834
+
835
+ ### Custom Headers
836
+
837
+ ```scala
838
+ import zio.http.Header
839
+
840
+ val custom = Header.Custom("x-request-id", "abc-123")
841
+ custom.headerName // "x-request-id"
842
+ custom.renderedValue // "abc-123"
843
+ ```
844
+
845
+ ## Headers
846
+
847
+ `Headers` is a flat array-based collection with lazy monotonic parsing:
848
+
849
+ ```scala
850
+ final class Headers private[http] (
851
+ private val names: Array[String],
852
+ private val rawValues: Array[String],
853
+ private val parsed: Array[AnyRef], // null -> unparsed, value -> cached
854
+ val size: Int
855
+ )
856
+ ```
857
+
858
+ When you call `get[H]`, the header is parsed once and cached. Subsequent calls return the cached result.
859
+
860
+ ### Creating Headers
861
+
862
+ ```scala
863
+ import zio.http.Headers
864
+
865
+ val empty = Headers.empty
866
+
867
+ val headers = Headers(
868
+ "content-type" -> "application/json",
869
+ "authorization" -> "Bearer token",
870
+ "x-request-id" -> "abc-123"
871
+ )
872
+ ```
873
+
874
+ ### Getting Typed Headers
875
+
876
+ ```scala
877
+ import zio.http.{Headers, *}
878
+ import zio.http.{headers => h}
879
+
880
+ val headers = Headers(
881
+ "content-type" -> "application/json",
882
+ "content-length" -> "1024"
883
+ )
884
+
885
+ val ct = headers.get(h.ContentType)
886
+ // Some(h.ContentType(...)) (parsed and cached)
887
+
888
+ val cl = headers.get(h.ContentLength)
889
+ // Some(h.ContentLength(1024)) (parsed and cached)
890
+
891
+ val auth = headers.get(h.Authorization)
892
+ // None (not present)
893
+ ```
894
+
895
+ ### Getting Raw Values
896
+
897
+ ```scala
898
+ import zio.http.Headers
899
+
900
+ val headers = Headers("x-custom" -> "value")
901
+
902
+ headers.rawGet("x-custom") // Some("value")
903
+ headers.rawGet("missing") // None
904
+ ```
905
+
906
+ ### Getting All Headers of a Type
907
+
908
+ Some headers can appear multiple times (like `Set-Cookie`):
909
+
910
+ ```scala
911
+ import zio.http.{Headers, *}
912
+ import zio.http.{headers => h}
913
+
914
+ val headers = Headers(
915
+ "set-cookie" -> "session=abc",
916
+ "set-cookie" -> "preference=dark"
917
+ )
918
+
919
+ val cookies = headers.getAll(h.SetCookieHeader)
920
+ // Chunk(h.SetCookieHeader(...), h.SetCookieHeader(...))
921
+ ```
922
+
923
+ ### Modifying Headers
924
+
925
+ ```scala
926
+ import zio.http.Headers
927
+
928
+ val headers = Headers("a" -> "1", "b" -> "2")
929
+
930
+ val added = headers.add("c", "3") // adds "c: 3"
931
+ val set = headers.set("a", "100") // replaces all "a" values
932
+ val removed = headers.remove("b") // removes all "b" entries
933
+ val has = headers.has("a") // true
934
+ ```
935
+
936
+ ### Converting to List
937
+
938
+ ```scala
939
+ import zio.http.Headers
940
+
941
+ val headers = Headers("a" -> "1", "b" -> "2")
942
+ headers.toList // List(("a", "1"), ("b", "2"))
943
+ ```
944
+
945
+ ### Combining Headers
946
+
947
+ ```scala
948
+ import zio.http.Headers
949
+
950
+ val auth = Headers("authorization" -> "Bearer token")
951
+ val cors = Headers("access-control-allow-origin" -> "*")
952
+
953
+ val combined = auth ++ cors // Headers(authorization: ..., access-control-allow-origin: ...)
954
+
955
+ combined.contains("authorization") // true (alias for `has`)
956
+ combined.toChunk // Chunk(("authorization", ...), ("access-control-allow-origin", ...))
957
+ ```
958
+
959
+ ## Body
960
+
961
+ `Body` wraps a materialized `Chunk[Byte]` with a content type:
962
+
963
+ ```scala
964
+ final class Body private (
965
+ val data: Chunk[Byte],
966
+ val contentType: ContentType
967
+ )
968
+ ```
969
+
970
+
971
+ ### Creating Bodies
972
+
973
+ `Body.empty` provides an empty body with default `application/octet-stream` content type:
974
+
975
+ ```scala
976
+ import zio.http.Body
977
+
978
+ val empty = Body.empty
979
+ // Body(data = Chunk.empty, contentType = application/octet-stream)
980
+ ```
981
+
982
+ `Body.fromString` creates a body with `text/plain` content type:
983
+
984
+ ```scala
985
+ import zio.http.{Body, Charset}
986
+
987
+ val fromString = Body.fromString("Hello, World!", Charset.UTF8)
988
+ // Content-Type: text/plain; charset=UTF-8
989
+ ```
990
+
991
+ `Body.fromArray` creates a body with default `application/octet-stream` content type:
992
+
993
+ ```scala
994
+ import zio.http.Body
995
+
996
+ val fromBytes = Body.fromArray(Array[Byte](1, 2, 3))
997
+ // Content-Type: application/octet-stream
998
+ ```
999
+
1000
+ `Body.fromChunk` creates a body from a `Chunk[Byte]` with optional content type:
1001
+
1002
+ ```scala
1003
+ import zio.http.{Body, ContentType}
1004
+ import zio.blocks.chunk.Chunk
1005
+ import zio.blocks.mediatype.MediaTypes
1006
+
1007
+ val chunk = Chunk[Byte](1, 2, 3, 4, 5)
1008
+ val body = Body.fromChunk(chunk)
1009
+ // Content-Type: application/octet-stream (default)
1010
+
1011
+ val jsonBody = Body.fromChunk(chunk, ContentType(MediaTypes.application.`json`))
1012
+ // Content-Type: application/json
1013
+ ```
1014
+
1015
+ ### Reading Bodies
1016
+
1017
+ `Body` provides direct access to data and content type:
1018
+
1019
+ ```scala
1020
+ import zio.http.{Body, Charset}
1021
+
1022
+ val body = Body.fromString("Hello!", Charset.UTF8)
1023
+
1024
+ body.length // 6
1025
+ body.isEmpty // false
1026
+ body.nonEmpty // true
1027
+ body.asString() // "Hello!" (UTF-8 default)
1028
+ body.asString(Charset.ASCII) // "Hello!" (explicit charset)
1029
+ body.data // Chunk[Byte](72, 101, 108, 108, 111, 33)
1030
+ body.contentType // ContentType(text/plain; charset=UTF-8)
1031
+ ```
1032
+
1033
+ ## Cookie
1034
+
1035
+ Cookies are split into `RequestCookie` and `ResponseCookie` with different structures:
1036
+
1037
+ ```scala
1038
+ final case class RequestCookie(name: String, value: String)
1039
+
1040
+ final case class ResponseCookie(
1041
+ name: String,
1042
+ value: String,
1043
+ domain: Option[String],
1044
+ path: Option[Path],
1045
+ maxAge: Option[Long],
1046
+ isSecure: Boolean,
1047
+ isHttpOnly: Boolean,
1048
+ sameSite: Option[SameSite]
1049
+ )
1050
+ ```
1051
+
1052
+ ### SameSite
1053
+
1054
+ ```scala
1055
+ import zio.http.SameSite
1056
+
1057
+ val strict = SameSite.Strict
1058
+ val lax = SameSite.Lax
1059
+ val none = SameSite.None_ // underscore avoids conflict with scala.None
1060
+ ```
1061
+
1062
+ ### Parsing Request Cookies
1063
+
1064
+ ```scala
1065
+ import zio.http.Cookie
1066
+
1067
+ val cookies = Cookie.parseRequest("session=abc123; preference=dark")
1068
+ // Chunk(RequestCookie("session", "abc123"), RequestCookie("preference", "dark"))
1069
+ ```
1070
+
1071
+ ### Parsing Response Cookies
1072
+
1073
+ ```scala
1074
+ import zio.http.Cookie
1075
+
1076
+ val cookie = Cookie.parseResponse("session=abc; Domain=example.com; Path=/; Secure; HttpOnly; SameSite=Strict")
1077
+ // Right(ResponseCookie(
1078
+ // name = "session",
1079
+ // value = "abc",
1080
+ // domain = Some("example.com"),
1081
+ // path = Some(Path("/")),
1082
+ // isSecure = true,
1083
+ // isHttpOnly = true,
1084
+ // sameSite = Some(SameSite.Strict)
1085
+ // ))
1086
+ ```
1087
+
1088
+ ### Rendering Cookies
1089
+
1090
+ ```scala
1091
+ import zio.http.{Cookie, RequestCookie, ResponseCookie, SameSite, Path}
1092
+ import zio.blocks.chunk.Chunk
1093
+
1094
+ val requestCookies = Chunk(
1095
+ RequestCookie("session", "abc"),
1096
+ RequestCookie("theme", "dark")
1097
+ )
1098
+ Cookie.renderRequest(requestCookies)
1099
+ // "session=abc; theme=dark"
1100
+
1101
+ val responseCookie = ResponseCookie(
1102
+ name = "session",
1103
+ value = "xyz",
1104
+ domain = Some("example.com"),
1105
+ path = Some(Path("/")),
1106
+ maxAge = Some(3600),
1107
+ isSecure = true,
1108
+ isHttpOnly = true,
1109
+ sameSite = Some(SameSite.Strict)
1110
+ )
1111
+ Cookie.renderResponse(responseCookie)
1112
+ // "session=xyz; Domain=example.com; Path=/; Max-Age=3600; Secure; HttpOnly; SameSite=Strict"
1113
+ ```
1114
+
1115
+ ## Form
1116
+
1117
+ `Form` represents URL-encoded form data:
1118
+
1119
+ ```scala
1120
+ final case class Form(entries: Chunk[(String, String)])
1121
+ ```
1122
+
1123
+ ### Creating Forms
1124
+
1125
+ ```scala
1126
+ import zio.http.Form
1127
+
1128
+ val empty = Form.empty
1129
+
1130
+ val form = Form(
1131
+ "username" -> "alice",
1132
+ "password" -> "secret",
1133
+ "remember" -> "true"
1134
+ )
1135
+ ```
1136
+
1137
+ ### Accessing Form Data
1138
+
1139
+ ```scala
1140
+ import zio.http.Form
1141
+
1142
+ val form = Form(
1143
+ "tag" -> "scala",
1144
+ "tag" -> "functional",
1145
+ "page" -> "1"
1146
+ )
1147
+
1148
+ form.get("tag") // Some("scala") (first value)
1149
+ form.getAll("tag") // Chunk("scala", "functional")
1150
+ form.get("page") // Some("1")
1151
+ form.get("missing") // None
1152
+ ```
1153
+
1154
+ ### Modifying Forms
1155
+
1156
+ ```scala
1157
+ import zio.http.Form
1158
+
1159
+ val form = Form("a" -> "1")
1160
+ val added = form.add("b", "2") // Form(("a", "1"), ("b", "2"))
1161
+ ```
1162
+
1163
+ ### Encoding and Parsing
1164
+
1165
+ ```scala
1166
+ import zio.http.Form
1167
+
1168
+ val form = Form(
1169
+ "name" -> "Alice Smith",
1170
+ "email" -> "alice@example.com"
1171
+ )
1172
+
1173
+ val encoded = form.encode
1174
+ // "name=Alice%20Smith&email=alice%40example.com"
1175
+
1176
+ val parsed = Form.fromString(encoded)
1177
+ // Form with decoded entries
1178
+ ```
1179
+
1180
+ ## FormField
1181
+
1182
+ `FormField` is a sealed trait for multipart form fields, supporting simple key-value pairs, text parts with optional metadata, and binary parts:
1183
+
1184
+ ```scala
1185
+ import zio.http._
1186
+ import zio.blocks.chunk.Chunk
1187
+ import zio.blocks.mediatype.MediaTypes
1188
+
1189
+ // Simple key-value field
1190
+ val simple = FormField.Simple("username", "alice")
1191
+
1192
+ // Text field with optional content type and filename
1193
+ val text = FormField.Text(
1194
+ name = "bio",
1195
+ value = "Hello world",
1196
+ contentType = Some(ContentType(MediaTypes.text.`plain`)),
1197
+ filename = None
1198
+ )
1199
+
1200
+ // Binary field with content type and optional filename
1201
+ val binary = FormField.Binary(
1202
+ name = "avatar",
1203
+ data = Chunk.fromArray(Array[Byte](1, 2, 3)),
1204
+ contentType = ContentType(MediaTypes.image.`png`),
1205
+ filename = Some("avatar.png")
1206
+ )
1207
+
1208
+ // All variants share a common `name` accessor
1209
+ simple.name // "username"
1210
+ text.name // "bio"
1211
+ binary.name // "avatar"
1212
+ ```
1213
+
1214
+ ## Request
1215
+
1216
+ `Request` combines all HTTP request components:
1217
+
1218
+ ```scala
1219
+ final case class Request(
1220
+ method: Method,
1221
+ url: URL,
1222
+ headers: Headers,
1223
+ body: Body,
1224
+ version: Version
1225
+ )
1226
+ ```
1227
+
1228
+ ### Creating Requests
1229
+
1230
+ ```scala
1231
+ import zio.http._
1232
+
1233
+ val url = URL.parse("https://api.example.com/users").toOption.get
1234
+ val getRequest = Request.get(url)
1235
+
1236
+ val jsonBody = Body.fromString("""{"name":"Alice"}""", Charset.UTF8)
1237
+ val postRequest = Request.post(url, jsonBody)
1238
+ ```
1239
+
1240
+ ### Request Factory Methods
1241
+
1242
+ ```scala
1243
+ import zio.http._
1244
+
1245
+ val url = URL.parse("https://api.example.com/resource/1").toOption.get
1246
+ val body = Body.fromString("""{"name":"updated"}""")
1247
+
1248
+ Request.get(url) // GET with empty body
1249
+ Request.post(url, body) // POST with body
1250
+ Request.put(url, body) // PUT with body
1251
+ Request.patch(url, body) // PATCH with body
1252
+ Request.delete(url) // DELETE with empty body
1253
+ Request.head(url) // HEAD with empty body
1254
+ Request.options(url) // OPTIONS with empty body
1255
+ ```
1256
+
1257
+ ### Modifying Requests
1258
+
1259
+ All modification methods return a new `Request`—the original is unchanged:
1260
+
1261
+ ```scala
1262
+ import zio.http._
1263
+
1264
+ val request = Request.get(URL.parse("https://api.example.com/users").toOption.get)
1265
+
1266
+ // Adding/modifying headers
1267
+ request.addHeader("Accept", "application/json")
1268
+ request.addHeaders(Headers("X-A" -> "1", "X-B" -> "2"))
1269
+ request.setHeader("Accept", "text/html") // replaces existing
1270
+ request.removeHeader("Accept")
1271
+
1272
+ // Replacing components
1273
+ request.body(Body.fromString("data"))
1274
+ request.url(URL.parse("/other").toOption.get)
1275
+ request.method(Method.POST)
1276
+ request.version(Version.`HTTP/2.0`)
1277
+ // Functional updates
1278
+ request.updateHeaders(_.add("X-Custom", "value"))
1279
+ request.updateUrl(_ / "123") // appends path segment
1280
+ ```
1281
+
1282
+ Full control:
1283
+
1284
+ ```scala
1285
+ import zio.http._
1286
+
1287
+ val url = URL.parse("https://api.example.com/users").toOption.get
1288
+ val body = Body.fromString("""{"name":"Alice"}""", Charset.UTF8)
1289
+
1290
+ val request = Request(
1291
+ method = Method.POST,
1292
+ url = url,
1293
+ headers = Headers(
1294
+ "content-type" -> "application/json",
1295
+ "authorization" -> "Bearer token123"
1296
+ ),
1297
+ body = body,
1298
+ version = Version.`HTTP/1.1`
1299
+ )
1300
+ ```
1301
+
1302
+ ### Accessing Request Data
1303
+
1304
+ ```scala
1305
+ import zio.http._
1306
+ import zio.http.{headers => h}
1307
+
1308
+ val request = Request.get(URL.parse("/api/users?page=1").toOption.get)
1309
+
1310
+ request.path // Path("/api/users")
1311
+ request.queryParams // QueryParams("page" -> "1")
1312
+ request.contentType // Option[ContentType]
1313
+ request.header(h.Authorization) // Option[h.Authorization]
1314
+ ```
1315
+
1316
+ ## Response
1317
+
1318
+ `Response` represents HTTP responses:
1319
+
1320
+ ```scala
1321
+ final case class Response(
1322
+ status: Status,
1323
+ headers: Headers,
1324
+ body: Body,
1325
+ version: Version
1326
+ )
1327
+ ```
1328
+
1329
+ ### Creating Responses
1330
+
1331
+ ```scala
1332
+ import zio.http._
1333
+
1334
+ val ok = Response.ok // 200 OK, empty body
1335
+
1336
+ val notFound = Response.notFound // 404 Not Found, empty body
1337
+ ```
1338
+
1339
+ ### Response Factory Methods
1340
+
1341
+ ```scala
1342
+ import zio.http._
1343
+
1344
+ // Status-only responses
1345
+ Response.ok // 200
1346
+ Response.notFound // 404
1347
+ Response.badRequest // 400
1348
+ Response.unauthorized // 401
1349
+ Response.forbidden // 403
1350
+ Response.internalServerError // 500
1351
+ Response.serviceUnavailable // 503
1352
+
1353
+ // Responses with bodies
1354
+ Response.text("Hello, World!") // 200 with text/plain body
1355
+ Response.json("""{'ok':true}""") // 200 with application/json in headers and body
1356
+ // Redirects
1357
+ Response.redirect("/new-location") // 307 Temporary Redirect
1358
+ Response.redirect("/new-location", isPermanent = true) // 308 Permanent Redirect
1359
+ Response.seeOther("/other") // 303 See Other
1360
+ ```
1361
+
1362
+ Note that `Response.json` creates bodies with `application/json` content-type on the `Body` itself, not just in the headers.
1363
+
1364
+ ### Modifying Responses
1365
+
1366
+ ```scala
1367
+ import zio.http._
1368
+
1369
+ val response = Response.ok
1370
+
1371
+ // Adding/modifying headers
1372
+ response.addHeader("X-Custom", "value")
1373
+ response.setHeader("X-Custom", "new-value")
1374
+ response.removeHeader("X-Custom")
1375
+
1376
+ // Replacing components
1377
+ response.body(Body.fromString("data"))
1378
+ response.status(Status.Created)
1379
+ response.version(Version.`HTTP/2.0`)
1380
+ // Functional update
1381
+ response.updateHeaders(_.add("X-Request-Id", "abc"))
1382
+
1383
+ // Add a Set-Cookie header
1384
+ response.addCookie(ResponseCookie("session", "abc123"))
1385
+ ```
1386
+
1387
+ Full control:
1388
+
1389
+ ```scala
1390
+ import zio.http._
1391
+
1392
+ val jsonBody = Body.fromString("""{"message":"created"}""", Charset.UTF8)
1393
+
1394
+ val response = Response(
1395
+ status = Status.Created,
1396
+ headers = Headers(
1397
+ "content-type" -> "application/json",
1398
+ "location" -> "/users/123"
1399
+ ),
1400
+ body = jsonBody,
1401
+ version = Version.`HTTP/1.1`
1402
+ )
1403
+ ```
1404
+
1405
+ ### Accessing Response Data
1406
+
1407
+ ```scala
1408
+ import zio.http._
1409
+ import zio.http.{headers => h}
1410
+
1411
+ val response = Response.ok
1412
+
1413
+ response.status.code // 200
1414
+ response.status.isSuccess // true
1415
+ response.contentType // Option[ContentType]
1416
+ response.header(h.ContentType) // Option[h.ContentType]
1417
+ response.header(h.Location) // Option[h.Location]
1418
+ ```
1419
+
1420
+ ## Advanced Usage
1421
+
1422
+ ### Building a Complete HTTP Exchange
1423
+
1424
+ ```scala
1425
+ import zio.http._
1426
+
1427
+ // Build request
1428
+ val url = URL.parse("https://api.example.com/users").toOption.get
1429
+ val requestBody = Body.fromString("""{"name":"Alice","age":30}""", Charset.UTF8)
1430
+
1431
+ val request = Request(
1432
+ method = Method.POST,
1433
+ url = url,
1434
+ headers = Headers(
1435
+ "content-type" -> "application/json",
1436
+ "authorization" -> "Bearer abc123",
1437
+ "user-agent" -> "MyClient/1.0"
1438
+ ),
1439
+ body = requestBody,
1440
+ version = Version.`HTTP/1.1`
1441
+ )
1442
+
1443
+ // Build response
1444
+ val responseBody = Body.fromString("""{"id":123,"name":"Alice","age":30}""", Charset.UTF8)
1445
+
1446
+ val response = Response(
1447
+ status = Status.Created,
1448
+ headers = Headers(
1449
+ "content-type" -> "application/json",
1450
+ "location" -> "/users/123"
1451
+ ),
1452
+ body = responseBody,
1453
+ version = Version.`HTTP/1.1`
1454
+ )
1455
+ ```
1456
+
1457
+ ### URL Building with Fluent API
1458
+
1459
+ ```scala
1460
+ import zio.http._
1461
+
1462
+ val url = URL.parse("https://api.example.com").toOption.get
1463
+
1464
+ val extended = (url / "v1" / "users" / "123") ?? ("include", "profile") ?? ("include", "posts")
1465
+
1466
+ extended.encode
1467
+ // "https://api.example.com/v1/users/123?include=profile&include=posts"
1468
+ ```
1469
+
1470
+ ### Typed Header Access
1471
+
1472
+ ```scala
1473
+ import zio.http._
1474
+ import zio.http.{headers => h}
1475
+
1476
+ val headers = Headers(
1477
+ "content-type" -> "application/json; charset=utf-8",
1478
+ "content-length" -> "1024",
1479
+ "authorization" -> "Bearer token"
1480
+ )
1481
+
1482
+ // Type-safe header access with parsing
1483
+ val ct = headers.get(h.ContentType)
1484
+ ct.map(_.value.charset) // Some(Some(Charset.UTF8))
1485
+
1486
+ val cl = headers.get(h.ContentLength)
1487
+ cl.map(_.length) // Some(1024)
1488
+
1489
+ // Raw access
1490
+ headers.rawGet("authorization") // Some("Bearer token")
1491
+ ```
1492
+
1493
+ ### Cookie Management
1494
+
1495
+ ```scala
1496
+ import zio.http._
1497
+ import zio.blocks.chunk.Chunk
1498
+
1499
+ // Parse cookies from request header
1500
+ val cookieHeader = "session=abc; theme=dark"
1501
+ val requestCookies = Cookie.parseRequest(cookieHeader)
1502
+
1503
+ // Create response with Set-Cookie headers
1504
+ val sessionCookie = ResponseCookie(
1505
+ name = "session",
1506
+ value = "xyz123",
1507
+ path = Some(Path("/")),
1508
+ maxAge = Some(3600),
1509
+ isSecure = true,
1510
+ isHttpOnly = true,
1511
+ sameSite = Some(SameSite.Strict)
1512
+ )
1513
+
1514
+ val response = Response(
1515
+ status = Status.Ok,
1516
+ headers = Headers(
1517
+ "set-cookie" -> Cookie.renderResponse(sessionCookie)
1518
+ ),
1519
+ body = Body.empty
1520
+ )
1521
+ ```
1522
+
1523
+ ### Form Submission
1524
+
1525
+ ```scala
1526
+ import zio.http._
1527
+
1528
+ val form = Form(
1529
+ "username" -> "alice",
1530
+ "password" -> "secret",
1531
+ "remember" -> "true"
1532
+ )
1533
+
1534
+ val formBody = Body.fromString(form.encode, Charset.UTF8)
1535
+
1536
+ val request = Request(
1537
+ method = Method.POST,
1538
+ url = URL.parse("/login").toOption.get,
1539
+ headers = Headers(
1540
+ "content-type" -> "application/x-www-form-urlencoded"
1541
+ ),
1542
+ body = formBody,
1543
+ version = Version.`HTTP/1.1`
1544
+ )
1545
+ ```
1546
+
1547
+ ## Design Principles
1548
+
1549
+ ### Single Encoding Contract
1550
+
1551
+ `Path` and `QueryParams` store decoded values internally. Encoding happens only at output boundaries:
1552
+
1553
+ - `Path.fromEncoded(s)` decodes, stores decoded segments
1554
+ - `Path.encode` encodes segments for transmission
1555
+ - `QueryParams.fromEncoded(s)` decodes, stores decoded key-value pairs
1556
+ - `QueryParams.encode` encodes for transmission
1557
+
1558
+ This eliminates double-encoding bugs and clarifies responsibilities.
1559
+
1560
+ ### Lazy Header Parsing
1561
+
1562
+ `Headers` stores raw string values and parses typed headers on first access. Parsed results are cached in a parallel `Array[AnyRef]` for O(1) subsequent lookups. This design:
1563
+
1564
+ - Avoids parsing headers that are never accessed
1565
+ - Avoids re-parsing the same header multiple times
1566
+ - Supports unknown/custom headers without parsing failures
1567
+
1568
+ ### No Streaming
1569
+
1570
+ Bodies are fully materialized `Chunk[Byte]`. Streaming is left to higher-level HTTP libraries that compose with this data model. This keeps the model simple and effect-free.
1571
+
1572
+ ### Zero ZIO Dependency
1573
+
1574
+ The module uses `zio.blocks.chunk.Chunk` instead of `zio.Chunk`, making it usable in any Scala project without ZIO.
1575
+
1576
+ ## Schema-Based Typed Access (zio-http-model-schema)
1577
+
1578
+ The `zio-http-model-schema` module provides schema-based extraction of query parameters and headers with automatic decoding and validation.
1579
+
1580
+ ### Installation
1581
+
1582
+ Add the following to your `build.sbt`:
1583
+
1584
+ ```scala
1585
+ libraryDependencies += "dev.zio" %% "zio-blocks-http-model-schema" % "<version>"
1586
+ ```
1587
+
1588
+ For cross-platform projects (Scala.js):
1589
+
1590
+ ```scala
1591
+ libraryDependencies += "dev.zio" %%% "zio-blocks-http-model-schema" % "<version>"
1592
+ ```
1593
+
1594
+ ### Imports
1595
+
1596
+ Import the schema module to enable extension methods:
1597
+
1598
+ ```scala
1599
+ import zio.http.schema._
1600
+ import zio.blocks.schema.Schema
1601
+ ```
1602
+
1603
+ ### Query Parameter Extraction
1604
+
1605
+ `QueryParams` gains schema-based extraction methods via implicit conversions:
1606
+
1607
+ ```scala
1608
+ import zio.http.{QueryParams, URL}
1609
+ import zio.http.schema._
1610
+ import zio.blocks.schema.Schema
1611
+
1612
+ val url = URL.parse("/api/users?page=2&tag=scala&tag=fp").toOption.get
1613
+ val params = url.queryParams
1614
+
1615
+ // Extract single value with automatic decoding
1616
+ params.query[Int]("page")
1617
+ // Right(2)
1618
+
1619
+ // Extract all values for a key
1620
+ params.queryAll[String]("tag")
1621
+ // Right(Chunk("scala", "fp"))
1622
+
1623
+ // Extract with default fallback
1624
+ params.queryOrElse[Int]("limit", 10)
1625
+ // 10 - uses default since "limit" not present
1626
+ ```
1627
+
1628
+ ### Header Extraction
1629
+
1630
+ `Headers` gains schema-based extraction methods:
1631
+
1632
+ ```scala
1633
+ import zio.http.{Headers, Request, URL}
1634
+ import zio.http.schema._
1635
+ import zio.blocks.schema.Schema
1636
+
1637
+ val request = Request.get(URL.parse("/").toOption.get)
1638
+ .addHeader("x-page", "5")
1639
+ .addHeader("x-tag", "scala")
1640
+ .addHeader("x-tag", "functional")
1641
+
1642
+ val headers = request.headers
1643
+
1644
+ // Extract single header value
1645
+ headers.header[Int]("x-page")
1646
+ // Right(5)
1647
+
1648
+ // Extract all header values
1649
+ headers.headerAll[String]("x-tag")
1650
+ // Right(Chunk("scala", "functional"))
1651
+
1652
+ // Extract with default fallback
1653
+ headers.headerOrElse[Int]("x-limit", 100)
1654
+ // 100
1655
+ ```
1656
+
1657
+ ### Request and Response Extensions
1658
+
1659
+ `Request` and `Response` gain schema-based extraction methods that delegate to their query parameters and headers:
1660
+
1661
+ ```scala
1662
+ import zio.http.{Request, Response, URL}
1663
+ import zio.http.schema._
1664
+ import zio.blocks.schema.Schema
1665
+
1666
+ // Request query parameter extraction
1667
+ val request = Request.get(URL.parse("/search?q=zio&limit=20").toOption.get)
1668
+
1669
+ request.query[String]("q")
1670
+ // Right("zio")
1671
+
1672
+ request.query[Int]("limit")
1673
+ // Right(20)
1674
+
1675
+ // Response header extraction via schema
1676
+ val response = Response.ok.addHeader("x-correlation-id", "abc-123")
1677
+
1678
+ val responseOps = new ResponseSchemaOps(response)
1679
+ responseOps.header[String]("x-correlation-id")
1680
+ ```
1681
+
1682
+ ### Error Handling
1683
+
1684
+ Schema-based extraction returns `Either[QueryParamError, A]` or `Either[HeaderError, A]` for explicit error handling:
1685
+
1686
+ ```scala
1687
+ import zio.http.{QueryParams, URL}
1688
+ import zio.http.schema._
1689
+ import zio.blocks.schema.Schema
1690
+
1691
+ val params = QueryParams("name" -> "Alice", "age" -> "invalid")
1692
+
1693
+ params.query[String]("name") match {
1694
+ case Right(name) => println(s"Name: $name")
1695
+ case Left(QueryParamError.Missing(key)) => println(s"Missing key: $key")
1696
+ case Left(QueryParamError.Malformed(key, value, cause)) =>
1697
+ println(s"Failed to parse $key=$value: $cause")
1698
+ }
1699
+
1700
+ params.query[Int]("age") match {
1701
+ case Right(age) => println(s"Age: $age")
1702
+ case Left(QueryParamError.Missing(key)) => println(s"Missing key: $key")
1703
+ case Left(QueryParamError.Malformed(key, value, cause)) =>
1704
+ println(s"Failed to parse $key=$value: $cause")
1705
+ }
1706
+ ```
1707
+
1708
+ ### Supported Types
1709
+
1710
+ The schema module provides built-in `Schema` instances for common types. Any type with a `Schema[T]` can be extracted:
1711
+
1712
+ - **Primitives**: `String`, `Int`, `Long`, `Boolean`, `Double`, `Float`, `Short`, `Byte`, `Char`
1713
+ - **Big Numbers**: `BigInt`, `BigDecimal`
1714
+ - **UUID**: `java.util.UUID`
1715
+
1716
+ For custom types, define a `Schema[T]` instance using schema derivation or manual construction.