-
Notifications
You must be signed in to change notification settings - Fork 127
Open
Labels
enhancementNew feature or requestNew feature or request
Description
Motivation
ZIO HTTP currently mixes pure HTTP data types with runtime/server concerns (ZIO effects, Netty interop, streaming, codec logic). This creates several problems:
- Circular dependencies —
Status.toResponsecreates aResponse,Response.fromThrowabledepends onStatus, server factories live on data types - Non-portable —
java.nio.charset.Charsetin public API,java.util.LinkedHashMapbackingQueryParams, NettyAsciiStringviaCharSequenceleaking intoHeaders - Recurring bugs from design ambiguity — URL encoding has produced 6 bugs in the last year because
Pathhas no clear contract on whether segments are stored encoded or decoded - Allocation overhead —
Headersuses aConcattree that allocates on every++,Statusis a sealed trait with case objects instead of an unboxed int,Bodywraps everything in ZIO effects even for fully materialized data
We propose a new zio-blocks-http module: a pure, immutable, zero-ZIO-dependency HTTP data model optimized for extreme runtime performance. It depends only on other zio-blocks modules (chunk, mediatype). No streaming (will come when blocks Streams is ready). No server/client runtime assumptions — no Netty, no specific HTTP server dependency.
Design Principles
- Pure data — no effects, no streaming, no mutable state (except monotonic lazy-parse caches in Headers)
- Zero ZIO dependency — collections use blocks
Chunk, notzio.Chunk - Single encoding contract — Path segments always stored decoded, encoding happens only at serialization
- Typed headers with lazy parsing — keep zio-http's
Headersealed trait hierarchy for type safety, but parse lazily and cache - Server-runtime agnostic — no design choices assume Netty or any specific server. Adapters belong in zio-http.
- Scala 3 first, Scala 2 compatible — use opaque types where beneficial (Status), with AnyVal fallbacks
Components
| # | Type | Internal Representation | Key Design Decisions |
|---|---|---|---|
| 1 | Method |
Sealed trait enum | No ANY (routing wildcard, not HTTP). No ++. No / operator. Add ordinal: Int for array-indexed dispatch. |
| 2 | Status |
Opaque type over Int (Scala 3); AnyVal (Scala 2) |
Zero allocation. Status.Ok, Status.NotFound, etc. as plain vals — no cache needed. inline extension methods for isSuccess, isClientError, etc. No toResponse. |
| 3 | Version |
Simple enum: Http1_0, Http1_1, Http2, Http3 |
No Default (that's config, not protocol). |
| 4 | Scheme |
Simple enum: HTTP, HTTPS, WS, WSS |
Optional Custom(String) case for extensibility. |
| 5 | Path |
Chunk[String] of decoded segments + trailing-slash flag |
Single encoding contract: segments always stored decoded. Path("/users/john doe") from plain string. Path.fromEncoded("/users/john%20doe") from wire format. encode for output. One shared PercentEncoder, RFC 3986 compliant with per-component encoding rules. |
| 6 | QueryParams |
Parallel arrays: keys: Array[String], values: Array[Chunk[String]], size: Int |
No Java collections. No LinkedHashMap, no java.util.List. Immutable. Encoding via shared PercentEncoder. QueryParamsBuilder (mutable) for construction. Option semantics are NOT in QueryParams — that's the endpoint codec layer. |
| 7 | URL |
scheme, host, port, path, queryParams, fragment |
Relative vs absolute = presence/absence of scheme. No Location sealed trait. URL parsing as static method. |
| 8 | Header |
Sealed trait with typed subtypes (ContentType, Accept, Authorization, etc.) |
Keep zio-http's Header type hierarchy for type-safe access. Each HeaderType has parse(String): Either[String, H] and render(H): String — nothing else. No fromHeaders, no toHeaders, no StringSchemaCodec. Pure String → A and A → String. |
| 9 | Headers |
Parallel arrays: names[], values[], parsed[], size |
Flat, not a concat-tree. See Headers design detail below. |
| 10 | Body |
Array[Byte] + optional ContentType |
No effects. No Task, ZIO, ZStream. Always materialized. empty, fromArray, fromChunk, fromString. Streaming support will come with blocks Streams. |
| 11 | ContentType |
mediaType: MediaType + optional Boundary + optional Charset |
Uses MediaType from zio-blocks-mediatype. |
| 12 | Charset |
Own enum: UTF8, ASCII, ISO_8859_1, UTF16, UTF16BE, UTF16LE |
No java.nio.charset.Charset in public API. Portable to Scala.js. toJava conversion on JVM. |
| 13 | Request |
method, url, headers, body, version |
No remoteAddress, no remoteCertificate (transport concerns). No HeaderOps/QueryOps mixins. Immutable. |
| 14 | Response |
status, headers, body, version |
No mutable encoded cache. No fromSocketApp, fromThrowable, fromServerSentEvents. Immutable. |
| 15 | Cookie |
RequestCookie(name, value) and ResponseCookie(name, value, domain, path, maxAge, ...) |
Pure data. Encoding/decoding as standalone functions, not methods on the data type. |
| 16 | Form |
Chunk[(String, String)] |
URL-encoded form data. No multipart streaming. |
| 17 | Boundary |
Value wrapper over String |
Multipart boundary identifier. |
Headers Design Detail
The key design is typed headers with lazy parsing and caching:
┌───────────────────────────────────────────────────────┐
│ Headers │
│ │
│ names: [ "content-type", "accept", "x-custom" ] │ ← pre-lowercased
│ values: [ "application/json", "text/*", "foo" ] │ ← raw wire value
│ parsed: [ ContentType(…), null, null ] │ ← lazy, cached
│ size: 3 │
└───────────────────────────────────────────────────────┘
parsed[]starts all-null. Populated on first typed access. Monotonic writes (null → value), safe under races (deterministic parsing = same result if double-parsed).headers.get(Header.ContentType)→ scansnames[], checksparsed[i], parses if null, caches, returns typed value.headers.rawGet("content-type")→ scansnames[], returnsvalues[i], zero parsing.headers.getAll(Header.SetCookie)→ returns all matching entries. Multiple entries with the same name = multiple array slots — multi-value headers are naturally supported per RFC 9110 §5.2–5.3.headers.add(name, value)/headers.set(name, value)—addappends (multi-value),setreplaces all existing.- Server runtimes use
HeadersBuilder(mutable) to construct from wire data, then.build()into immutableHeaders.
Design Issues in zio-http This Addresses
Based on analysis of open/closed issues in zio/zio-http:
| Bug Class | zio-http Issues | Root Cause | How This Fixes It |
|---|---|---|---|
| URL double/missing encoding | zio/zio-http#3887, zio/zio-http#3935, zio/zio-http#3625, zio/zio-http#3624, zio/zio-http#3646 | Path has no contract on encoded vs decoded. Multiple independent encode/decode implementations. |
Single contract: segments stored decoded. One PercentEncoder, RFC 3986 per-component rules. |
| Multi-value header loss | zio/zio-http#3590 | Headers.Concat tree + getUnsafe returns first match only. |
Flat array — getAll naturally returns all matches. |
| Header performance | zio/zio-http#1063 | Concat tree traversal, Iterator allocation on encode. |
Flat array, indexed access, HeadersBuilder for zero-copy construction. |
| Missing headers / race | zio/zio-http#3395 | Mutable var encoded on Response + lazy tree iteration. |
Immutable. No mutable fields on Response. |
| Cookie replacement | zio/zio-http#3438 | addCookie removes then re-adds on Concat tree — non-atomic. |
Append-only flat array. |
| QueryParams Java collections | zio/zio-http#3420 | java.util.LinkedHashMap merge logic. |
Purpose-built parallel arrays. No Java collections. |
| Optional query double-wrap | zio/zio-http#3448, zio/zio-http#3491, zio/zio-http#2942 | StringSchemaCodec wrapping + HttpCodec.Fallback nesting overflow. |
QueryParams is raw data — no Option semantics baked in. |
| Non-portable Charset | zio/zio-http#3650 | java.nio.charset.Charset in public API. |
Own Charset enum with JVM conversion. |
| Basic auth colon bug | zio/zio-http#3193 | split(":") with length == 2 check in HeaderType. |
Clean parse/render functions per HeaderType. |
Module Dependencies
zio-blocks-chunk (zero deps)
zio-blocks-mediatype (zero deps)
zio-blocks-http (depends: chunk, mediatype)
zio-http (depends: zio-blocks-http, + ZIO + server runtime)
Non-Goals (Explicit Exclusions)
- No streaming body — will come with blocks
Streams - No server/client runtime — no
Handler,Route,Server,Client - No ZIO dependency — no
Task,ZIO,Chunk(from zio),ZStream - No endpoint/codec — separate
zio-blocks-endpointmodule (tracked in separate issue) - No OpenAPI generation — stays in zio-http
- No middleware —
Response.Patch,HandlerAspectetc. stay in zio-http - No WebSocket — server-runtime concern
- No multipart streaming — depends on streaming support
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request