Kora HTTP Server
Kora sub-skill — obey the kora-v1 meta rules on every task: R0 ensure
.kora-agent/docs+examples are cloned · R1 read this sub-skill before writing code · R2 Kora APIs only — no Spring/Micronaut/Quarkus, no invented annotations or config keys · R3 journal any incorrect Kora usage. Add comments/Javadoc only if asked.
Declarative HTTP request handlers compiled (not reflected) into a router. Annotate a @Component @HttpController with @HttpRoute methods; Kora generates the handler/router at build time via the annotation processor.
The base path lives on @HttpRoute, not on @HttpController — @HttpController takes no path argument.
Quick Start
1. Dependencies (Java)
configurations {
koraBom
annotationProcessor.extendsFrom(koraBom)
implementation.extendsFrom(koraBom)
}
dependencies {
koraBom platform("ru.tinkoff.kora:kora-parent:1.2.19")
annotationProcessor "ru.tinkoff.kora:annotation-processors" // mandatory
implementation "ru.tinkoff.kora:http-server-undertow"
implementation "ru.tinkoff.kora:json-module"
implementation "ru.tinkoff.kora:config-hocon"
implementation "ru.tinkoff.kora:logging-logback"
}
Kotlin uses KSP instead: ksp "ru.tinkoff.kora:symbol-processors". All ru.tinkoff.kora:*
artifacts inherit their version from the BOM — never pin them individually.
2. Application graph
@KoraApp
public interface Application extends
HoconConfigModule,
JsonModule,
LogbackModule,
UndertowHttpServerModule {
static void main(String[] args) {
KoraApplication.run(ApplicationGraph::graph);
}
}
3. Controller
@Component
@HttpController
public final class HelloController {
@HttpRoute(method = HttpMethod.GET, path = "/hello/{name}")
public String hello(@Path String name) {
return "Hello " + name; // 200 OK, text/plain
}
}
4. Config (application.conf)
httpServer {
publicApiHttpPort = 8080 // application traffic
privateApiHttpPort = 8085 // metrics / probes
telemetry.logging.enabled = true
}
CRUD controller
Adapted from kora-java-guide-http-server-app. JSON requires @Json on the method and on the
body parameter; optional inputs are marked @Nullable (Kotlin: a nullable type).
@Component
@HttpController
public final class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@HttpRoute(method = HttpMethod.GET, path = "/users/{userId}")
@Json
public UserResponse getUser(@Path String userId) {
return userService.getUser(userId)
.orElseThrow(() -> HttpServerResponseException.of(404, "User not found: " + userId));
}
@HttpRoute(method = HttpMethod.GET, path = "/users")
@Json
public List<UserResponse> getUsers(@Nullable @Query("page") Integer page,
@Nullable @Query("size") Integer size) {
return userService.getUsers(page == null ? 0 : page, size == null ? 10 : size);
}
@HttpRoute(method = HttpMethod.POST, path = "/users")
@Json
public HttpResponseEntity<UserResponse> createUser(@Json UserRequest request) {
UserResponse user = userService.createUser(request);
return HttpResponseEntity.of(201, HttpHeaders.of("Location", "/users/" + user.id()), user);
}
@HttpRoute(method = HttpMethod.DELETE, path = "/users/{userId}")
public HttpServerResponse deleteUser(@Path String userId) {
userService.deleteUser(userId);
return HttpServerResponse.of(204, HttpBody.empty());
}
}
DTOs are plain records annotated @Json:
UserRequest(String email, String name), UserResponse(String id, String email, String name).
Key annotations
| Annotation | Level | Purpose |
|---|---|---|
@HttpController |
class | Marks an HTTP controller (no path argument) |
@HttpRoute(method, path) |
method | Binds an HttpMethod + path to a handler |
@Path |
parameter | Path segment {name}; name defaults to the argument name |
@Query |
parameter | Query parameter; name defaults to the argument name |
@Header |
parameter | Request header value |
@Cookie |
parameter | Cookie value |
@Json |
method / parameter | JSON serialization for the body |
@Mapping(X.class) |
parameter / method | Custom request/response mapper |
@InterceptWith(X.class) |
method / class | Apply an interceptor to a route or controller |
@Nullable |
parameter | Marks a request parameter optional (Java) |
HttpMethod values: GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD, TRACE.
Response types
| Return type | Status | Notes |
|---|---|---|
String / byte[] / ByteBuffer |
200 | Body written directly with the matching content type |
T + @Json on method |
200 | Body serialized to JSON |
HttpResponseEntity<T> + @Json |
any | Body + custom status code + headers |
HttpServerResponse |
any | Full control over status, headers and raw body |
// JSON body with custom status and headers
return HttpResponseEntity.of(201, HttpHeaders.of("Location", "/users/" + id), user);
// Full manual control
return HttpServerResponse.of(200, HttpHeaders.of("X-Trace", traceId), HttpBody.plaintext("OK"));
return HttpServerResponse.of(204, HttpBody.empty());
Async signatures are supported: Java CompletionStage<T> / Mono<T>, Kotlin suspend fun.
See Response Types for HttpBody, HttpHeaders,
and custom HttpServerResponseMapper.
Error handling
Throw HttpServerResponseException.of(code, message) from a handler to short-circuit with a
status code. Centralize cross-cutting error translation in a global interceptor (there is no
dedicated "exception handler" type — error handling is an interceptor concern).
@HttpRoute(method = HttpMethod.GET, path = "/users/{id}")
@Json
public UserResponse get(@Path String id) {
return userService.find(id)
.orElseThrow(() -> HttpServerResponseException.of(404, "Not found: " + id));
}
See Error Handling for the global error interceptor.
Interceptors
HttpServerInterceptor.intercept(Context, HttpServerRequest, InterceptChain) returns
CompletionStage<HttpServerResponse>. Call chain.process(context, request) to continue the
chain; wrap it with .whenComplete(...) / .exceptionally(...) for after/error logic.
@Tag(HttpServerModule.class) // makes it global (only one global interceptor allowed)
@Component
public final class LoggingInterceptor implements HttpServerInterceptor {
private static final Logger log = LoggerFactory.getLogger(LoggingInterceptor.class);
@Override
public CompletionStage<HttpServerResponse> intercept(Context context,
HttpServerRequest request,
InterceptChain chain) throws Exception {
long started = System.nanoTime();
return chain.process(context, request).whenComplete((response, error) -> {
long ms = (System.nanoTime() - started) / 1_000_000;
int code = response != null ? response.code() : 500;
log.info("{} {} -> {} ({} ms)", request.method(), request.path(), code, ms);
});
}
}
Scopes:
- Global — one interceptor tagged
@Tag(HttpServerModule.class), runs for every route. - Controller —
@InterceptWith(X.class)on the controller class. - Method —
@InterceptWith(X.class)on a single@HttpRoutemethod.
Order: global -> controller -> method -> handler.
See Interceptors.
Authentication & Principal
For custom authentication in Kora 1.2.x, use the interceptor pattern — do NOT use Principal as a controller parameter (not auto-wired in 1.2.x, causes 401→400 downgrade).
Pattern: Global @Tag(HttpServerModule) interceptor → store principal in Context → read via Principal.current() in controller.
See Authentication & Principal for complete guide with examples.
Context Propagation
Pass computed values from interceptor to controller (auth session, user profile, request metadata) via ru.tinkoff.kora.common.Context:
- Define
Context.Key<T>static singleton - Interceptor sets value BEFORE
chain.process() - Controller reads with null-check
See Context Propagation for complete guide.
Configuration
httpServer {
publicApiHttpPort = 8080
privateApiHttpPort = 8085
privateApiHttpMetricsPath = "/metrics"
privateApiHttpReadinessPath = "/system/readiness"
privateApiHttpLivenessPath = "/system/liveness"
virtualThreadsEnabled = false // true requires Java 24+
maxRequestBodySize = "256MiB"
telemetry {
logging { enabled = false }
metrics { enabled = true }
tracing { enabled = true }
}
}
Keep publicApiHttpPort (traffic) and privateApiHttpPort (metrics/probes) separate.
See Configuration for every key and env substitution.
References
| Reference | Covers |
|---|---|
| Controller & Routing | @HttpController, @HttpRoute, path composition |
| Request Mapping | @Path, @Query, @Header, @Cookie, @Json, bodies, @Mapping |
| Response Types | HttpServerResponse, HttpResponseEntity, HttpBody, mappers |
| Interceptors | HttpServerInterceptor, @InterceptWith, @Tag, order |
| Error Handling | HttpServerResponseException, global error interceptor |
| Configuration | httpServer keys, ports, telemetry, env vars |
| Authentication & Principal | Custom auth via interceptor, Principal pattern (Kora 1.2.x) |
| Context Propagation | Passing values from interceptor to controller via Context |
Assets
| Template | Purpose |
|---|---|
| UserController.java | CRUD controller (Java) |
| UserController.kt | CRUD controller (Kotlin) |
| LoggingInterceptor.java | Global logging interceptor (Java) |
| LoggingInterceptor.kt | Global logging interceptor (Kotlin) |
| ErrorInterceptor.java | Global error-to-JSON interceptor (Java) |
Pitfalls
| Symptom | Fix |
|---|---|
| 404 on a valid URL | The {var} in @HttpRoute path must have a matching @Path argument; base path lives on @HttpRoute, not @HttpController |
| Request body is null / not parsed | Add @Json on the method and on the body parameter, and depend on json-module |
@HttpController("/api") does not compile |
@HttpController takes no value; put the prefix in each @HttpRoute(path = "/api/...") |
| Required parameter throws when missing | Mark it @Nullable (Java) or use a nullable type (Kotlin) |
| Global interceptor never runs | Add @Tag(HttpServerModule.class) and @Component; only one global interceptor is allowed |
Looking for HttpServerResponse.ok() builder |
It does not exist — use HttpServerResponse.of(...) / HttpResponseEntity.of(...) |
| Principal as controller parameter returns 400 | Kora 1.2.x doesn't auto-bridge HttpServerPrincipalExtractor — use interceptor + Context pattern (see Authentication) |
| Context.get() returns null in controller | Value must be set in interceptor BEFORE chain.process(); add null-check (see Context Propagation) |
Evals
Self-check rubric: evals/evals.json.