You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat!: Handlers return Response value object; remove ResponseBuilder
Handlers now return an immutable Response record instead of mutating
an HttpExchange via the ResponseBuilder fluent API. Response is a pure
value: status + body + contentType + headers, with factories for
common shapes (empty/status/ok/of/text/bytes/stream).
ResponseDecorator becomes a (Request, Response) -> Response transform
applied after the handler returns; interceptors and continuations
return Response too so cross-cutting work composes cleanly.
Removed: ResponseBuilder, DefaultResponseBuilder, Request.respond(int),
the per-Request responseSent flag, and the IllegalStateException
state machine - single-shot is enforced by the return type.
Renderer (internal ResponseRenderer) handles byte[] / BodyWriter /
mapper.writeTo dispatch, including null-body -> responseLength=-1.
Copy file name to clipboardExpand all lines: README.md
+70-65Lines changed: 70 additions & 65 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -26,16 +26,15 @@ It is designed to be simple to use while providing the essential features needed
26
26
27
27
### Basic Usage
28
28
1. Create an OpenAPI specification file named `openapi.json` in your project resources.
29
-
2. Define your handlers using the `RequestHandler` functional interface:
29
+
2. Define your handlers using the `RequestHandler` functional interface. Handlers are pure functions: they consume a `Request` and return a `Response`. The framework renders the response (status code, headers, body) for you.
30
30
```java
31
31
// Inline lambda — returns JSON using the built-in Gson mapper.
A `null` body always produces a status-only response (`Content-Length: 0`, no body bytes), regardless of status code. Streaming bodies bypass `TypeMapper` entirely; one-shot object bodies (`ok`, `of`) are serialised by the `TypeMapper` registered for the response's content type (default `application/json`).
79
+
53
80
3. Initialize the server:
54
81
```java
55
82
publicclassYourServerLauncher {
@@ -122,60 +149,46 @@ User-supplied mappers take precedence over built-in defaults, so you can overrid
122
149
123
150
### Response decorators
124
151
125
-
`Builder.responseDecorator(...)` registers a `ResponseDecorator` that runs whenever a handler calls `request.respond(status)`. Decorators set headers (or other pre-terminal state) before the handler reaches a terminal call. They run in registration order, and handler-supplied headers override decorator-supplied ones (last write wins).
126
-
127
-
Use cases: stamping correlation IDs, tenant IDs, server identifiers, or any cross-cutting header on every response.
152
+
`Builder.responseDecorator(...)` registers a `ResponseDecorator` — a `(Request, Response) -> Response` transform applied to every handler's return value before rendering. Decorators compose in registration order: the result of one is fed to the next. Decorator-supplied headers override handler-supplied ones; if you want the opposite, set the header inside the handler with `Response.withHeader(...)`.
`ResponseDecorator` is a `@FunctionalInterface`; the lambda receives the `Request` and the `ResponseBuilder` that's about to be returned from `respond(...)`. Don't call a terminal method (`empty()` / `bytes()` / `json()` / ...) from a decorator — terminals belong to the handler.
142
-
143
163
### Request interceptors
144
164
145
-
`Builder.interceptor(...)` registers a `RequestInterceptor` that wraps every handler invocation. Use it for `ScopedValue` bindings, MDC, authentication, tracing, or any concern that needs to run uniformly around handlers. Interceptors compose in registration order: the first registered runs outermost.
165
+
`Builder.interceptor(...)` registers a `RequestInterceptor` that wraps every handler invocation. Use it for `ScopedValue` bindings, MDC, authentication, tracing, or any concern that needs to run uniformly around handlers. Interceptors compose in registration order: the first registered runs outermost. Each interceptor must call `next.proceed()` and return the result (or a transformed `Response`).
146
166
147
167
```java
148
168
OpenApiServer.builder()
149
169
.spec(spec)
150
170
.handlers(handlers)
151
-
.interceptor(
152
-
(request, next) -> {
153
-
// Resolve once per request.
154
-
String tenant = request.header("X-Tenant-Id");
155
-
ScopedValue.where(TENANT, tenant)
156
-
.call(
157
-
() -> {
158
-
next.proceed();
159
-
returnnull;
160
-
});
161
-
})
162
-
.interceptor(
163
-
(request, next) -> {
164
-
MDC.put("op", request.operationId());
165
-
try {
166
-
next.proceed();
167
-
} finally {
168
-
MDC.remove("op");
169
-
}
170
-
})
171
+
.interceptor((request, next) -> {
172
+
// Resolve once per request; bind to a ScopedValue for the rest of the chain.
Each interceptor must call `next.proceed()` to continue the chain. Exceptions propagate to the library's standard `ExceptionFilter` and `ExceptionHandler` pipeline.
187
+
Exceptions propagate to the library's standard `ExceptionFilter` and `ExceptionHandler` pipeline.
175
188
176
189
### Combining interceptors and decorators
177
190
178
-
The two collaborate naturally: the interceptor binds per-request context once, and the decorator reads that context when stamping response headers. Handlers stay free of cross-cutting code.
191
+
The two collaborate naturally: the interceptor binds per-request context once, and the decorator reads that context when stamping response headers. Handlers stay pure business logic.
179
192
180
193
```java
181
194
// Per-request context populated by the interceptor, read by the decorator and handlers.
@@ -186,44 +199,36 @@ OpenApiServer.builder()
186
199
.spec(spec)
187
200
.handlers(handlers)
188
201
// 1. Resolve once per request and bind to ScopedValues.
Inside any handler, `CORRELATION_ID.get()` / `TENANT_ID.get()` return the resolved values — no parameter threading, no static accessors. Because the decorator runs *inside* the interceptor's `ScopedValue` binding (decorators fire on `request.respond(...)`, which the handler calls while the interceptor's `proceed()`is still on the stack), the `get()`calls always see the bound value.
218
+
Decorators run inside the interceptor's `ScopedValue` binding (the decorator transforms the `Response` returned by `next.proceed()`, which is still on the call stack), so `CORRELATION_ID.get()`/ `TENANT_ID.get()` see the bound values.
- Built-in `GsonJsonMapper` auto-registered when Gson is on the classpath (no explicit wiring needed)
335
340
-`ResponseDecorator` for cross-cutting response headers and `RequestInterceptor` for around-style ScopedValue / MDC / auth concerns
336
341
- Built on Java's native `HttpServer` with Thread-Per-Request behaviour using Virtual Threads
@@ -358,6 +363,6 @@ A few things to know:
358
363
-**Single-process model.** No horizontal scaling primitives are bundled; run multiple instances behind a load balancer for production scale.
359
364
-**JDK HttpServer is the throughput ceiling.** It's documented as a low-throughput / dev-test server. If you need to go materially above the rates above, deploy the same filter/validator/router stack on Jetty, Helidon Níma, or Netty — the spec and validation code is server-agnostic.
360
365
-**Per-request state uses `ScopedValue`** (Java 25, JEP 506). This matters if a handler offloads work to an executor that's not a `StructuredTaskScope`-managed child thread: the `ScopedValue` is not visible there, so the handler must capture the values it needs (e.g. `byte[] body = request.bytes();`) before submitting.
361
-
-**Empty responses must use `request.respond(status).empty()`**, which sends `responseLength = -1` (`Content-Length: 0`, no body). Passing `0` produces a chunked response with zero chunks, which is technically non-conformant.
366
+
-**Empty responses use `Response.empty()` (204) or `Response.status(code)` for other no-body statuses.** The renderer sends `responseLength = -1` (`Content-Length: 0`, no body) for any `Response` with `body() == null`, regardless of status code. Passing `0`to the JDK directly produces a chunked response with zero chunks, which is technically non-conformant — `Response` factories handle this for you.
0 commit comments