Kora облачно ориентированный серверный фреймворк написанный на Java для написания Java / Kotlin приложений с упором на производительность, эффективность, прозрачность сделанный выходцами из Т-Банк / Тинькофф

Kora is a cloud-oriented server-side Java framework for writing Java / Kotlin applications with a focus on performance, efficiency and transparency

Skip to content
V1 V2

HTTP server

The HTTP server module describes the incoming HTTP boundary of an application: accepting a request, parsing parameters, reading the body, selecting a handler, creating a response, telemetry, and interceptors. In Kora, controllers can be described declaratively with @HttpController and @HttpRoute, or handlers can be registered imperatively with HttpServerRequestHandler.

The declarative approach fits most APIs: the method signature describes the HTTP contract, and Kora creates the handler at compile time without using Reflection at runtime. The imperative approach is useful for low-level or dynamic routes where it is easier to process the request manually.

Recommendation

We recommend using an approach where OpenAPI file is primary contract and controllers are created from it using a OpenAPI generator. This approach allows you to achieve consistency between the consumer and owner of the contract and allows you to share this contract to create clients for it using the same approach. For more information about the generator, see the section on generating from OpenAPI.

For a step-by-step walkthrough before the reference details, see HTTP Server and Advanced HTTP Server.

Dependency

Implementation is based on Undertow. Undertow is a lightweight open-source web server for Java applications. It is built on asynchronous and non-blocking I/O operations using NIO, which ensures high performance and low resource consumption.

Dependency build.gradle:

implementation "ru.tinkoff.kora:http-server-undertow"

Module:

@KoraApp
public interface Application extends UndertowHttpServerModule { }

Dependency build.gradle.kts:

implementation("ru.tinkoff.kora:http-server-undertow")

Module:

@KoraApp
interface Application : UndertowHttpServerModule

Configuration

Basic HTTP server configuration parameters:

httpServer {
    publicApiHttpPort = 8080 //(1)!
    privateApiHttpPort = 8085 //(2)!
    virtualThreadsEnabled = false //(3)!
    maxRequestBodySize = "256MiB" //(4)!
}
  1. Public HTTP server port (default: 8080)
  2. Private HTTP server port (default: 8085)
  3. Enables virtual threads for blocking request processing instead of the blockingThreads pool, requires Java 21+ (default: false)
  4. Maximum allowed size of incoming request body (default: 256MiB)
httpServer:
  publicApiHttpPort: 8080 #(1)!
  privateApiHttpPort: 8085 #(2)!
  virtualThreadsEnabled: false #(3)!
  maxRequestBodySize: "256MiB" #(4)!
  1. Public HTTP server port (default: 8080)
  2. Private HTTP server port (default: 8085)
  3. Enables virtual threads for blocking request processing instead of the blockingThreads pool, requires Java 21+ (default: false)
  4. Maximum allowed size of incoming request body (default: 256MiB)
Full Configuration

Example of the complete configuration described in the HttpServerConfig class (default or example values are specified):

httpServer {
    publicApiHttpPort = 8080 //(1)!
    privateApiHttpPort = 8085 //(2)!
    privateApiHttpMetricsPath = "/metrics" //(3)!
    privateApiHttpReadinessPath = "/system/readiness" //(4)!
    privateApiHttpLivenessPath = "/system/liveness" //(5)!
    ignoreTrailingSlash = false //(6)!
    ioThreads = 2 //(7)!
    blockingThreads = 2 //(8)!
    shutdownWait = "30s" //(9)!
    threadKeepAliveTimeout = "60s" //(10)!
    socketReadTimeout = "0s" //(11)!
    socketWriteTimeout = "0s" //(12)!
    socketKeepAliveEnabled = false //(13)!
    virtualThreadsEnabled = false //(14)!
    maxRequestBodySize = "256MiB" //(15)!
    telemetry {
        logging {
            enabled = false //(16)!
            stacktrace = true //(17)!
            mask = "***" //(18)!
            maskQueries = [ ] //(19)!
            maskHeaders = [ "authorization", "cookie", "set-cookie" ] //(20)!
            pathTemplate = true //(21)!
        }
        metrics {
            enabled = true //(22)!
            slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(23)!
            tags = { // (24)!
                "key1" = "value1"
                "key2" = "value2"
            }
        }
        tracing {
            enabled = true //(25)!
            attributes = { // (26)!
                "key1" = "value1"
                "key2" = "value2"
            }
        }
    }
}
  1. Public HTTP server port (default: 8080)
  2. Private HTTP server port (default: 8085)
  3. Path to get metrics on the private server (default: /metrics)
  4. Path to get readiness probe status on the private server (default: /system/readiness)
  5. Path to get liveness probe status on the private server (default: /system/liveness)
  6. Whether to ignore a trailing / in the path: when enabled, /my/path and /my/path/ are treated as the same route (default: false)
  7. Number of network I/O threads (default: number of available processors, but not less than 2)
  8. Number of threads for blocking request processing (default: min(max(available processors, 2) * 8, 200))
  9. Time to wait for processing before server shutdown during graceful shutdown (default: 30s)
  10. Maximum idle lifetime of a request handler thread (default: 60s)
  11. Maximum time to wait for reading data from a socket or connection; 0s disables the timeout (default: 0s)
  12. Maximum time to wait for writing data to a socket or connection; 0s disables the timeout (default: 0s)
  13. Whether to enable TCP keep-alive for a socket or connection (default: false)
  14. Enables virtual threads for blocking request processing instead of the blockingThreads pool, requires Java 21+ (default: false)
  15. Maximum allowed size of an incoming request body (default: 256MiB)
  16. Enables module logging (default: false)
  17. Enables call stack logging on exception (default: true)
  18. Mask used to hide specified headers and request or response parameters (default: ***)
  19. List of request parameters to hide (default: [])
  20. List of request or response headers to hide (default: [ "authorization", "cookie", "set-cookie" ])
  21. Whether to use the request path template in logs; when not specified, the template is always used except at TRACE, where the full path is used (default not specified, optional)
  22. Enables module metrics (default: true)
  23. Configures SLO for metrics (default: ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO)
  24. Configures metric tags (default: {})
  25. Enables module tracing (default: true)
  26. Configures tracing attributes (default: {})
httpServer:
  publicApiHttpPort: 8080 #(1)!
  privateApiHttpPort: 8085 #(2)!
  privateApiHttpMetricsPath: "/metrics" #(3)!
  privateApiHttpReadinessPath: "/system/readiness" #(4)!
  privateApiHttpLivenessPath: "/system/liveness" #(5)!
  ignoreTrailingSlash: false #(6)!
  ioThreads: 2 #(7)!
  blockingThreads: 2 #(8)!
  shutdownWait: "30s" #(9)!
  threadKeepAliveTimeout: "60s" #(10)!
  socketReadTimeout: "0s" #(11)!
  socketWriteTimeout: "0s" #(12)!
  socketKeepAliveEnabled: false #(13)!
  virtualThreadsEnabled: false #(14)!
  maxRequestBodySize: "256MiB" #(15)!
  telemetry:
    logging:
      enabled: false #(16)!
      stacktrace: true #(17)!
      mask: "***" #(18)!
      maskQueries: [ ] #(19)!
      maskHeaders: [ "authorization", "cookie", "set-cookie" ] #(20)!
      pathTemplate: true #(21)!
    metrics:
      enabled: true #(22)!
      slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(23)!
      tags: #(24)!
        key1: value1
        key2: value2
    tracing:
      enabled: true #(25)!
      attributes: #(26)!
        key1: value1
        key2: value2
  1. Public HTTP server port (default: 8080)
  2. Private HTTP server port (default: 8085)
  3. Path to get metrics on the private server (default: /metrics)
  4. Path to get readiness probe status on the private server (default: /system/readiness)
  5. Path to get liveness probe status on the private server (default: /system/liveness)
  6. Whether to ignore a trailing / in the path: when enabled, /my/path and /my/path/ are treated as the same route (default: false)
  7. Number of network I/O threads (default: number of available processors, but not less than 2)
  8. Number of threads for blocking request processing (default: min(max(available processors, 2) * 8, 200))
  9. Time to wait for processing before server shutdown during graceful shutdown (default: 30s)
  10. Maximum idle lifetime of a request handler thread (default: 60s)
  11. Maximum time to wait for reading data from a socket or connection; 0s disables the timeout (default: 0s)
  12. Maximum time to wait for writing data to a socket or connection; 0s disables the timeout (default: 0s)
  13. Whether to enable TCP keep-alive for a socket or connection (default: false)
  14. Enables virtual threads for blocking request processing instead of the blockingThreads pool, requires Java 21+ (default: false)
  15. Maximum allowed size of an incoming request body (default: 256MiB)
  16. Enables module logging (default: false)
  17. Enables call stack logging on exception (default: true)
  18. Mask used to hide specified headers and request or response parameters (default: ***)
  19. List of request parameters to hide (default: [])
  20. List of request or response headers to hide (default: [ "authorization", "cookie", "set-cookie" ])
  21. Whether to use the request path template in logs; when not specified, the template is always used except at TRACE, where the full path is used (default not specified, optional)
  22. Enables module metrics (default: true)
  23. Configures SLO for metrics (default: ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO)
  24. Configures metric tags (default: {})
  25. Enables module tracing (default: true)
  26. Configures tracing attributes (default: {})

Module metrics are described in the Metrics Reference section.

Public and private API

The module starts two independent HTTP servers:

  • Public API on publicApiHttpPort (default 8080) — serves all your @HttpController routes and imperative handlers. This is the port clients call.
  • Private API on privateApiHttpPort (default 8085) — serves operational endpoints that must not be exposed to external traffic: metrics (privateApiHttpMetricsPath, default /metrics), and the readiness and liveness probes (privateApiHttpReadinessPath / privateApiHttpLivenessPath, defaults /system/readiness and /system/liveness).

Separating the ports lets you publish only the public port through your ingress/load balancer while keeping metrics and probes reachable only from the internal network or the orchestrator (for example Kubernetes). The private endpoints are provided by the respective modules (Metrics, Probes) — you do not register them manually.

Native server configuration

For low-level tuning that the configuration above does not cover, Kora exposes two Undertow-specific extension points. Provide either as a component and Kora will apply it while building the server.

UndertowConfigurer gives direct access to the Undertow.Builder (worker/buffer settings, listeners, socket options, and other server-level options):

@Component
public final class SomeUndertowConfigurer implements UndertowConfigurer {

    @Override
    public Undertow.Builder configure(Undertow.Builder builder) {
        return builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true);
    }
}
@Component
class SomeUndertowConfigurer : UndertowConfigurer {

    override fun configure(builder: Undertow.Builder): Undertow.Builder {
        return builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true)
    }
}

HttpHandlerConfigurer wraps the root HttpHandler, which is the correct place to add cross-cutting behavior at the raw Undertow level (for example wrapping every request into an additional handler before Kora's routing runs):

@Component
public final class SomeHandlerConfigurer implements HttpHandlerConfigurer {

    @Override
    public HttpHandler configure(HttpHandler handler) {
        return exchange -> {
            // custom logic before Kora routing
            handler.handleRequest(exchange);
        };
    }
}
@Component
class SomeHandlerConfigurer : HttpHandlerConfigurer {

    override fun configure(handler: HttpHandler): HttpHandler {
        return HttpHandler { exchange ->
            // custom logic before Kora routing
            handler.handleRequest(exchange)
        }
    }
}

Prefer the Configuration parameters and interceptors for anything they can express; reach for these interfaces only for behavior that is genuinely Undertow-specific.

SomeController declarative

The @HttpController annotation should be used to create a controller, and the @Component annotation should be used to register it as a dependency. The @HttpRoute annotation is responsible for specifying the HTTP path and method for a particular handler method.

@Component //(1)!
@HttpController //(2)!
public final class SomeController {

    //(3)!
    @HttpRoute(method = HttpMethod.POST,  //(4)!
               path = "/hello/world")  //(5)!
    public String helloWorld() {
        return "Hello World";
    }
}
  1. Indicates that the class is a component and should be registered in the application dependency container
  2. Indicates that the class is a controller and contains HTTP handlers
  3. Indicates that the method is a path handler in the controller
  4. Indicates the type of the handler HTTP method
  5. Indicates the path of the handler method
@Component //(1)!
@HttpController //(2)!
class SomeController {

    //(3)!
    @HttpRoute(method = HttpMethod.POST,  //(4)!
               path = "/hello/world") //(5)!
    fun helloWorld(): String {
        return "Hello World"
    }
}
  1. Indicates that the class is a component and should be registered in the application dependency container
  2. Indicates that the class is a controller and contains HTTP handlers
  3. Indicates that the method is a path handler in the controller
  4. Indicates the type of the handler HTTP method
  5. Indicates the path of the handler method

Routing

@HttpRoute matches a request by its method (an HTTP method from HttpMethod) and its path. The path is a template that must start with / and may contain one or more {name} segments — each is a path parameter bound with @Path:

  • /users — a static path
  • /users/{id} — one path parameter
  • /users/{userId}/orders/{orderId} — several path parameters, including in the middle of the path

A path parameter always matches exactly one path segment; a value never spans a /.

Trailing slash. By default the match is exact, so /users and /users/ are different routes — a request to /users/ against a /users route returns 404. To treat them as the same route, enable httpServer.ignoreTrailingSlash (see Configuration):

httpServer {
    ignoreTrailingSlash = true
}
httpServer:
  ignoreTrailingSlash: true

Method matching. A path served with a method that has no matching route returns 405 Method Not Allowed; an unknown path returns 404 Not Found. The matched template is available at runtime as HttpServerRequest.route() and is used as the low-cardinality path label in metrics and tracing (see Telemetry).

Request

This section describes how an HTTP request is converted into controller method arguments. Special annotations are used for request parts, and the request body is passed as an argument without such an annotation.

String parameter conversion

Values from paths, query parameters, headers, and cookie arrive as strings. Kora uses StringParameterReader<T> to convert a string into the target type:

public interface StringParameterReader<T> {
    T read(String string);
}

StringParameterReader<T> is looked up as a graph component by the exact parameter type. If the parameter is declared as List<T> or Set<T>, the converter is applied to every value separately.

Out of the box, Kora supports String, Boolean, Integer, Long, Float, Double, UUID, BigInteger, BigDecimal, Duration, LocalDate, LocalTime, LocalDateTime, OffsetTime, OffsetDateTime, ZonedDateTime, and enum. For enum, the default mapping uses the value name via Enum.name(). If a value cannot be converted, the request is completed with a 400 response through HttpServerResponseException.

public record UserId(long value) {}

@Module
public interface UserIdModule {

    default StringParameterReader<UserId> userIdStringParameterReader() {
        return StringParameterReader.of(
            value -> new UserId(Long.parseLong(value)),
            value -> "Invalid user id: " + value
        );
    }
}
data class UserId(val value: Long)

@Module
interface UserIdModule {

    fun userIdStringParameterReader(): StringParameterReader<UserId> {
        return StringParameterReader.of(
            { value -> UserId(value.toLong()) },
            { value -> "Invalid user id: $value" }
        )
    }
}

After registering the converter, the custom type can be used in controller parameters:

@HttpRoute(method = HttpMethod.GET, path = "/users/{id}")
public User get(@Path("id") UserId id) {
    return userService.get(id);
}

Path parameter

@Path - denotes the value of the request path part, the parameter itself is specified in {path} in the path and the name of the parameter is specified in value or defaults to the name of the method argument. The value is converted through StringParameterReader<T>, so both built-in and custom types can be used.

@Component
@HttpController
public final class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/{pathName}")
    public String helloWorld(@Path("pathName") String pathValue) {
        return "Hello World";
    }
}
@Component
@HttpController
class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/{pathName}")
    fun helloWorld(
        @Path("pathName") pathValue: String
    ): String {
        return "Hello World";
    }
}

Query parameter

@Query - value of the query parameter, the name of the parameter is specified in value or is equal to the name of the method argument by default. Single values, List<T>, and Set<T> are supported. List<T> keeps all parameter values, while Set<T> removes duplicates and preserves the order of first occurrence.

@Component
@HttpController
public final class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    public String helloWorld(@Query("queryName") String queryValue,
                             @Query("queryNameList") List<String> queryValues) {
        return "Hello World";
    }
}
@Component
@HttpController
class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun helloWorld(
        @Query("queryName") queryValue: String,
        @Query("queryNameList") queryValues: List<String>
    ): String {
        return "Hello World";
    }
}

Request header

@Header - value of request header, the parameter name is specified in value or defaults to the method argument name. Single values, List<T>, and Set<T> are supported. List<T> and Set<T> use all values of the header.

@Component
@HttpController
public final class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    public String helloWorld(@Header("headerName") String headerValue,
                             @Header("headerNameList") List<String> headerValues) {
        return "Hello World";
    }
}
@Component
@HttpController
class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    operator fun helloWorld(
        @Header("headerName") headerValue: String,
        @Header("headerNameList") headerValues: List<String>
    ): String {
        return "Hello World";
    }
}

Request body

Specifying the request body requires using a method argument without special annotations. By default, byte[], ByteBuffer, String, FormUrlEncoded, FormMultipart, and custom types through HttpServerRequestMapper<T> are supported.

JSON

In request body should be treated as JSON, than it must be annotated via @Json tag to require handler for injection with JsonReader<T>.

@Component
@HttpController
public final class SomeController {

    public record Request(String name) {}

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    public String helloWorld(@Json Request body) { //(1)!
        return "Hello World";
    }
}
  1. Specifies that the body should be read as JSON
@Component
@HttpController
class SomeController {

    data class Request(val name: String)

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun helloWorld(@Json body: Request): String { //(1)!
        return "Hello World"
    }
}
  1. Specifies that the body should be read as JSON

The JSON module is required.

Form UrlEncoded

Declare a FormUrlEncoded (from ru.tinkoff.kora.http.common.form) argument to accept a request with the application/x-www-form-urlencoded content type (form data). No @Json or @Mapping annotation is needed — Kora has a built-in reader for this type.

FormUrlEncoded is iterable and provides:

  • get(String name) — returns the FormUrlEncoded.FormPart with that name, or null
  • FormPart.name() — the field name
  • FormPart.values() — all values of that field (a field may be repeated in the form)
@Component
@HttpController
public final class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/form/encoded")
    public String handle(FormUrlEncoded body) {
        FormUrlEncoded.FormPart name = body.get("name"); //(1)!
        String firstName = (name != null) ? name.values().get(0) : null;
        return "Hello " + firstName;
    }
}
  1. Reads the name field from the submitted form
@Component
@HttpController
class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/form/encoded")
    fun handle(body: FormUrlEncoded): String {
        val name = body.get("name") //(1)!
        val firstName = name?.values()?.firstOrNull()
        return "Hello $firstName"
    }
}
  1. Reads the name field from the submitted form
Form Multipart

Declare a FormMultipart (from ru.tinkoff.kora.http.common.form) argument to accept a multipart/form-data request (binary form), typically used for file uploads. No @Json or @Mapping annotation is needed.

FormMultipart.parts() returns the list of parts, where each FormMultipart.FormPart is one of the following sealed subtypes:

  • MultipartData — a text field: name(), content() (String)
  • MultipartFile — a file loaded into memory: name(), fileName(), contentType(), content() (byte[])
  • MultipartFileStream — a streamed file: name(), fileName(), contentType(), content() (Flow.Publisher<ByteBuffer>)
@Component
@HttpController
public final class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/form/multipart")
    public String handle(FormMultipart body) {
        for (FormMultipart.FormPart part : body.parts()) {
            if (part instanceof FormMultipart.FormPart.MultipartData data) { //(1)!
                String value = data.content();
            } else if (part instanceof FormMultipart.FormPart.MultipartFile file) { //(2)!
                String fileName = file.fileName();
                String contentType = file.contentType();
                byte[] content = file.content();
            }
        }
        return "OK";
    }
}
  1. A text field of the form
  2. A file uploaded within the form
@Component
@HttpController
class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/form/multipart")
    fun handle(body: FormMultipart): String {
        for (part in body.parts()) {
            when (part) {
                is FormMultipart.FormPart.MultipartData -> { //(1)!
                    val value = part.content()
                }
                is FormMultipart.FormPart.MultipartFile -> { //(2)!
                    val fileName = part.fileName()
                    val contentType = part.contentType()
                    val content = part.content()
                }
                else -> {}
            }
        }
        return "OK"
    }
}
  1. A text field of the form
  2. A file uploaded within the form

@Cookie - Cookie value, the parameter name is specified in value or defaults to the method argument name. The value can be received as String, as a Cookie type with name, value, and attributes, or as another type through StringParameterReader<T>.

@Component
@HttpController
public final class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    public String helloWorld(@Cookie("cookieName") String cookieValue) {
        return "Hello World";
    }
}
@Component
@HttpController
class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    operator fun helloWorld(
        @Cookie("cookieName") cookieValue: String
    ): String {
        return "Hello World";
    }
}

Custom parameter

If a method argument needs to be assembled from the request manually, use the HttpServerRequestMapper<T> interface. This is useful for user context, authorization, complex header validation, or several request parts at once:

@Component
@HttpController
public final class SomeController {

    public record UserPrincipal(String userId, String traceId) {}

    public static final class RequestMapper implements HttpServerRequestMapper<UserPrincipal> {

        @Override
        public UserPrincipal apply(HttpServerRequest request) {
            return new UserPrincipal(request.headers().getFirst("x-user-id"), request.headers().getFirst("x-trace-id"));
        }
    }

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    public String get(@Mapping(RequestMapper.class) UserPrincipal context) {
        return "Hello World";
    }
}
@Component
@HttpController
class MapperRequestController {

    data class UserPrincipal(val userId: String?, val traceId: String?)

    class RequestMapper : HttpServerRequestMapper<UserPrincipal> {
        override fun apply(request: HttpServerRequest): UserPrincipal {
            return UserPrincipal(
                request.headers().getFirst("x-user-id"),
                request.headers().getFirst("x-trace-id")
            )
        }
    }

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun get(@Mapping(RequestMapper::class) context: UserPrincipal): String {
        return "Hello World"
    }
}

Required parameters

By default, all arguments declared in a method are required. If a required value is missing in the request, Kora returns a 400 response.

By default, all method arguments that do not use the Kotlin Nullability syntax are required. If a required value is missing in the request, Kora returns a 400 response.

Optional parameters

If a method argument is optional, meaning it may be missing in the request, use @Nullable or Optional<T> for single values:

@Component
@HttpController
public final class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    public String helloWorld(@Nullable @Query("queryName") String queryValue) { //(1)!
        return "Hello World";
    }
}
  1. Any @Nullable annotation will do, for example javax.annotation.Nullable, jakarta.annotation.Nullable, or org.jetbrains.annotations.Nullable.

Use the Kotlin Nullability syntax and mark such a parameter as optional:

@Component
@HttpController
class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun helloWorld(@Query("queryName") queryValue: String?): String {
        return "Hello World"
    }
}

Response

By default, standard return value types can be used: byte[], ByteBuffer, String. They are processed with status 200 and the corresponding response content type header.

If the status, headers, or body must be specified manually, the method can return HttpServerResponse. The main HttpServerResponse contract consists of a response code, headers, and an optional body:

public interface HttpServerResponse {
    int code();
    MutableHttpHeaders headers();
    @Nullable
    HttpBodyOutput body();
}
@Component
@HttpController
public final class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    public HttpServerResponse helloWorld() {
        return HttpServerResponse.of(
                200, //(1)!
                HttpHeaders.of("headerName", "headerValue"), //(2)!
                HttpBody.plaintext("Hello World") //(3)!
        ); 
    }
}
  1. HTTP response status code
  2. Response headers
  3. Response body
@Component
@HttpController
class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun helloWorld(): HttpServerResponse {
        return HttpServerResponse.of(
            200, //(1)!
            HttpHeaders.of("headerName", "headerValue"), //(2)!
            HttpBody.plaintext("Hello World") //(3)!
        )
    }
}
  1. HTTP response status code
  2. Response headers
  3. Response body

JSON

If the response should be returned as JSON, use the @Json annotation on the method. Kora will find or create JsonWriter<T> for the response type:

@Component
@HttpController
public final class SomeController {

    public record Response(String greeting) {}

    @Json //(1)!
    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    public Response helloWorld() {
        return new Response("Hello World");
    }
}
  1. Specifies that the response should be in JSON format
@Component
@HttpController
class SomeController {

    data class Response(val greeting: String)

    @Json //(1)!
    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun helloWorld(): Response {
        return Response("Hello World")
    }
}
  1. Specifies that the response should be in JSON format

The JSON module is required.

Response entity

If the body, headers, and response status code should be returned together, use HttpResponseEntity<T>, a wrapper around the response body.

Below is an example similar to the JSON example with the HttpResponseEntity wrapper:

@Component
@HttpController
public final class SomeController {

    public record Response(String greeting) {}

    @Json
    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    public HttpResponseEntity<Response> helloWorld() {
        return HttpResponseEntity.of(200, HttpHeaders.of("myHeader", "12345"), new Response("Hello World"));
    }
}
@Component
@HttpController
class SomeController {

    data class Response(val greeting: String)

    @Json
    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun helloWorld(): HttpResponseEntity<Response> {
        return HttpResponseEntity.of(200, HttpHeaders.of("myHeader", "12345"), Response("Hello World"));
    }
}

Respond exception

If processing should be interrupted and an error should be returned immediately, throw HttpServerResponseException. It is both an exception and an HttpServerResponse, so it can be thrown from a controller, service, or parameter converter.

The HttpServerResponseException.of(...) factory methods allow specifying the status code, response text, cause, and headers. The response body is written as text/plain; charset=utf-8.

@Component
@HttpController
public final class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/{pathName}")
    public String helloWorld(@Path("pathName") String pathValue) {
        if("null".equals(pathValue)) {
            throw HttpServerResponseException.of(400, "Bad request");
        }
        return "OK";
    }
}
@Component
@HttpController
class SomeController {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/{pathName}")
    fun helloWorld(@Path("pathName") pathValue: String): String {
        if ("null" == pathValue) {
            throw HttpServerResponseException.of(400, "Bad request")
        }
        return "OK"
    }
}

Custom response

If the response needs to be created in a custom way, use the HttpServerResponseMapper<T> interface. It receives Context, the original HttpServerRequest, and the controller method result, and returns a ready HttpServerResponse:

@Component
@HttpController
public final class SomeController {

    public record HelloWorldResponse(String greeting, String name) {}

    public static final class ResponseMapper implements HttpServerResponseMapper<HelloWorldResponse> {

        @Override
        public HttpServerResponse apply(Context ctx, HttpServerRequest request, HelloWorldResponse result) {
            return HttpServerResponse.of(200, HttpBody.plaintext(result.greeting() + " - " + result.name()));
        }
    }

    @Mapping(ResponseMapper.class)
    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    public HelloWorldResponse helloWorld() {
        return new HelloWorldResponse("Hello World", "Bob");
    }
}
@Component
@HttpController
class SomeController {

    data class HelloWorldResponse(val greeting: String, val name: String)

    class ResponseMapper : HttpServerResponseMapper<HelloWorldResponse> {
        override fun apply(ctx: Context, request: HttpServerRequest, result: HelloWorldResponse): HttpServerResponse {
            return HttpServerResponse.of(200, HttpBody.plaintext(result.greeting + " - " + result.name))
        }
    }

    @Mapping(ResponseMapper::class)
    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun helloWorld(): HelloWorldResponse {
        return HelloWorldResponse("Hello World", "Bob")
    }
}

Signatures

Available signatures for declarative HTTP handler methods out of the box:

The T refers to the type of the return value. It can be a body type (void, String, byte[], a @Json type, etc.), an HttpResponseEntity<T> to also set status and headers, or the full HttpServerResponse.

  • T myMethod()blocking: the handler runs on a thread from the blockingThreads pool (or a virtual thread when virtualThreadsEnabled is on, see Configuration)
  • CompletionStage<T> myMethod()non-blocking: the handler runs on an I/O thread and must not block it; see CompletionStage
  • Mono<T> myMethod()non-blocking via Project Reactor (requires the reactor-core dependency)

A null result (or void) produces an empty body with status 200, unless the return type is HttpServerResponse / HttpResponseEntity, which carry their own status.

By T we mean the type of the return value — a body type, an HttpResponseEntity<T>, or the full HttpServerResponse.

  • myMethod(): Tblocking: the handler runs on a thread from the blockingThreads pool (or a virtual thread when virtualThreadsEnabled is on, see Configuration)
  • suspend myMethod(): Tnon-blocking Kotlin Coroutine (requires the kotlinx-coroutines-core dependency as implementation)

A nullable return type (T?) is allowed for handlers that may produce an empty 200 body.

Choose a blocking signature when the handler calls blocking code (JDBC, blocking clients); choose a non-blocking one only when the whole call chain is non-blocking, otherwise you will stall the I/O threads.

Interceptors

Interceptors can be created to change behavior or add shared logic around request processing. Use the HttpServerInterceptor interface:

public interface HttpServerInterceptor {
    CompletionStage<HttpServerResponse> intercept(Context context, HttpServerRequest request, InterceptChain chain) throws Exception;

    interface InterceptChain {
        CompletionStage<HttpServerResponse> process(Context ctx, HttpServerRequest request) throws Exception;
    }
}

An interceptor receives the current Context, HttpServerRequest, and the chain of further processing. To pass the request further, call chain.process(context, request). If the interceptor returns a response itself, the controller handler is not called.

Interceptors can be used on:

  • Specific controller methods
  • Entire controller
  • All controllers at once: register the interceptor component with the @Tag(HttpServerModule.class) tag; there can be several global interceptors

Execution order:

Interceptors declared with @InterceptWith run in the order they are declared (top to bottom): controller-level interceptors wrap method-level ones, and within the same target the order follows the annotation order. Each interceptor may modify the request before chain.process(...), short-circuit by returning a response without calling the chain, or handle the result/exception afterwards.

Global interceptors registered with @Tag(HttpServerModule.class) have no guaranteed order between each other. If a strict order across global concerns is required (for example authorization must run before error mapping), do not register several global interceptors — implement a single global interceptor that invokes the concerns in the required order internally.

@Component
@HttpController
public final class SomeController {

    public static final class MethodInterceptor implements HttpServerInterceptor {

        @Override
        public CompletionStage<HttpServerResponse> intercept(Context context, 
                                                             HttpServerRequest request, 
                                                             InterceptChain chain) throws Exception {
            return chain.process(context, request);
        }
    }

    @InterceptWith(MethodInterceptor.class)
    @HttpRoute(method = HttpMethod.POST, path = "/intercepted")
    public String helloWorld() {
        return "Hello World";
    }
}
@Component
@HttpController
class SomeController {

    class MethodInterceptor : HttpServerInterceptor {

        override fun intercept(
            context: Context,
            request: HttpServerRequest,
            chain: HttpServerInterceptor.InterceptChain
        ): CompletionStage<HttpServerResponse> {
            return chain.process(context, request)
        }
    }

    @InterceptWith(MethodInterceptor::class)
    @HttpRoute(method = HttpMethod.POST, path = "/intercepted")
    fun helloWorld(): String {
        return "Hello World"
    }
}

Error handling

Error handling for all HTTP responses can also be implemented through an interceptor. Below is a simple example of such an interceptor.

@Tag(HttpServerModule.class)
@Component
public final class ErrorInterceptor implements HttpServerInterceptor {

    @Override
    public CompletionStage<HttpServerResponse> intercept(Context context, 
                                                         HttpServerRequest request, 
                                                         InterceptChain chain) throws Exception {
        return chain.process(context, request).exceptionally(e -> {
            if(e instanceof CompletionException) {
                e = e.getCause();
            }
            if (e instanceof HttpServerResponseException ex) {
                return ex;
            }

            var body = HttpBody.plaintext(e.getMessage());
            if (e instanceof IllegalArgumentException) {
                return HttpServerResponse.of(400, body);
            } else if (e instanceof TimeoutException) {
                return HttpServerResponse.of(408, body);
            } else {
                return HttpServerResponse.of(500, body);
            }
        });
    }
}
@Tag(HttpServerModule::class)
@Component
class ErrorInterceptor : HttpServerInterceptor {

    override fun intercept(
        context: Context,
        request: HttpServerRequest,
        chain: HttpServerInterceptor.InterceptChain
    ): CompletionStage<HttpServerResponse> {
        return chain.process(context, request).exceptionally { e ->
            val error = if (e is CompletionException) e.cause!! else e
            if (error is HttpServerResponseException) {
                return@exceptionally error
            }

            val body = HttpBody.plaintext(error.message)
            when (error) {
                is IllegalArgumentException -> HttpServerResponse.of(400, body)
                is TimeoutException -> HttpServerResponse.of(408, body)
                else -> HttpServerResponse.of(500, body)
            }
        }
    }
}

SomeController imperative

In order to create a controller, implement the HttpServerRequestHandler.HandlerFunction interface, and then register it in the HttpServerRequestHandler handler.

The following example shows how to handle all the described declarative request parameters from the examples above:

public interface SomeModule {

    default HttpServerRequestHandler someHttpHandler() {
        return HttpServerRequestHandlerImpl.of(HttpMethod.POST, //(1)!
                                               "/hello/{world}", //(2)!
                                               (context, request) -> {
            var path = RequestHandlerUtils.parseStringPathParameter(request, "world");
            var query = RequestHandlerUtils.parseOptionalStringQueryParameter(request, "query");
            var queries = RequestHandlerUtils.parseOptionalStringListQueryParameter(request, "Queries");
            var header = RequestHandlerUtils.parseOptionalStringHeaderParameter(request, "header");
            var headers = RequestHandlerUtils.parseOptionalStringListHeaderParameter(request, "Headers");
            return CompletableFuture.completedFuture(HttpServerResponse.of(200, HttpBody.plaintext("Hello World")));
        });
    }
}
  1. Specifies the HTTP method type of the handler method
  2. Indicates the path of the handler method
interface SomeModule {

    fun someHttpHandler(): HttpServerRequestHandler? {
        return HttpServerRequestHandlerImpl.of(
            HttpMethod.POST, //(1)!
            "/hello/{world}" //(2)!
        ) { context: Context, request: HttpServerRequest ->
            val path = RequestHandlerUtils.parseStringPathParameter(request, "world")
            val query = RequestHandlerUtils.parseOptionalStringQueryParameter(request, "query")
            val queries = RequestHandlerUtils.parseOptionalStringListQueryParameter(request, "Queries")
            val header = RequestHandlerUtils.parseOptionalStringHeaderParameter(request, "header")
            val headers = RequestHandlerUtils.parseOptionalStringListHeaderParameter(request, "Headers")
            CompletableFuture.completedFuture(HttpServerResponse.of(200, HttpBody.plaintext("Hello World")))
        }
    }
}
  1. Specifies the HTTP method type of the handler method
  2. Indicates the path of the handler method

Authorization

Kora provides a mechanism for extracting authorization context from HTTP requests via the HttpServerPrincipalExtractor interface. This interface allows implementing any authentication scheme: Basic/ApiKey/Bearer/OAuth.

How It Works

HttpServerPrincipalExtractor<T> extracts a token from the request (usually from the Authorization header) and returns a Principal object. The obtained Principal is stored in the request Context and can be retrieved anywhere during request processing via Principal.current().

public interface HttpServerPrincipalExtractor<T extends Principal> {
    CompletionStage<T> extract(HttpServerRequest request, @Nullable String value);
}

Where: - request — the current HTTP request, from which additional data (headers, parameters) can be extracted - value — the token value extracted from the Authorization header (or another source) - T extends Principal — the type of authorization context that will be stored in the Context

Custom Principal

Use can create a simple principal for API if needed with or without fields:

public record ApiPrincipal(String client) implements Principal {}
data class ApiPrincipal(val client: String) : Principal

To pass additional authorization information (userId, roles, scope), create a custom Principal implementation:

public record UserPrincipal(String userId, List<String> roles) implements Principal {}
data class UserPrincipal(val userId: String, val roles: List<String>) : Principal

If scope handling is required, use the PrincipalWithScopes interface:

public record ScopedUser(String userId, Collection<String> scopes) implements PrincipalWithScopes {}
data class ScopedUser(val userId: String, val scopes: Collection<String>) : PrincipalWithScopes

Basic Example

Simple example of extracting an API key from the Authorization header:

@Module
public interface AuthModule {

    @ConfigSource("auth.apiKey")
    interface ApiKeyAuthConfig {
        String value();
    }

    default HttpServerPrincipalExtractor<Principal> apiKeyExtractor(ApiKeyAuthConfig config) {
        return (request, value) -> {
            if (value == null || !config.value().equals(value)) {
                return CompletableFuture.failedFuture(
                    new IllegalAccessException("Invalid API key")
                );
            }
            return CompletableFuture.completedFuture(new ApiPrincipal("api-client"));
        };
    }
}
@Module
interface AuthModule {

    @ConfigSource("auth.apiKey")
    interface ApiKeyAuthConfig {
        fun value(): String
    }

    fun apiKeyExtractor(config: ApiKeyAuthConfig): HttpServerPrincipalExtractor<Principal> {
        return HttpServerPrincipalExtractor { request, value ->
            if (value == null || config.value() != value) {
                return@HttpServerPrincipalExtractor CompletableFuture.failedFuture(
                    IllegalAccessException("Invalid API key")
                )
            }
            CompletableFuture.completedFuture(ApiPrincipal("api-client"))
        }
    }
}

Bearer Token

Example of extracting a Bearer token with a custom Principal implementation:

@Module
public interface BearerAuthModule {

    default HttpServerPrincipalExtractor<UserPrincipal> bearerExtractor(TokenValidator validator) {
        return (request, value) -> {
            if (value == null || !value.startsWith("Bearer ")) {
                return CompletableFuture.failedFuture(
                    new IllegalAccessException("No Bearer token")
                );
            }

            String token = value.substring(7);
            return validator.validate(token)
                .thenApply(userData -> new UserPrincipal(userData.userId(), userData.roles()));
        };
    }
}
@Module
interface BearerAuthModule {

    fun bearerExtractor(validator: TokenValidator): HttpServerPrincipalExtractor<UserPrincipal> {
        return HttpServerPrincipalExtractor { request, value ->
            if (value == null || !value.startsWith("Bearer ")) {
                return CompletableFuture.failedFuture(
                    IllegalAccessException("No Bearer token")
                )
            }

            val token = value.substring(7)
            validator.validate(token)
                .thenApply { userData ->
                    UserPrincipal(userData.userId, userData.roles)
                }
        }
    }
}

Getting Principal

The current authorization context can be obtained anywhere during request processing:

@Component
@HttpController
public class SecureController {

    @HttpRoute(method = HttpMethod.GET, path = "/secure")
    public String getSecureData() {
        Principal principal = Principal.current();
        if (principal instanceof UserPrincipal user) {
            return "Hello, user: " + user.userId();
        }
        throw new SecurityException("Not authenticated");
    }
}
@Component
@HttpController
class SecureController {

    @HttpRoute(method = HttpMethod.GET, path = "/secure")
    fun getSecureData(): String {
        val principal = Principal.current()
        return if (principal is UserPrincipal) {
            "Hello, user: ${principal.userId}"
        } else {
            throw SecurityException("Not authenticated")
        }
    }
}

OAuth2

For OAuth2 authorization, create an HttpServerPrincipalExtractor that validates the token via an OAuth2 provider:

@Module
public interface OAuth2Module {

    default HttpServerPrincipalExtractor<ScopedUser> oauth2Extractor(OAuth2Client oauth2Client) {
        return (request, value) -> {
            if (value == null || !value.startsWith("Bearer ")) {
                return CompletableFuture.failedFuture(
                    new IllegalAccessException("No OAuth2 token")
                );
            }

            String token = value.substring(7);
            return oauth2Client.introspect(token)
                .thenApply(introspection -> 
                    new ScopedUser(
                        introspection.subject(),
                        introspection.scopes()
                    )
                );
        };
    }
}
@Module
interface OAuth2Module {

    fun oauth2Extractor(oauth2Client: OAuth2Client): HttpServerPrincipalExtractor<ScopedUser> {
        return HttpServerPrincipalExtractor { request, value ->
            if (value == null || !value.startsWith("Bearer ")) {
                return CompletableFuture.failedFuture(
                    IllegalAccessException("No OAuth2 token")
                )
            }

            val token = value.substring(7)
            oauth2Client.introspect(token)
                .thenApply { introspection ->
                    ScopedUser(introspection.subject, introspection.scopes)
                }
        }
    }
}

Scope Checking

To check scopes, create an interceptor that validates PrincipalWithScopes:

@Component
public final class ScopeCheckingInterceptor implements HttpServerInterceptor {

    private final String requiredScope;

    public ScopeCheckingInterceptor(@ConfigSource("auth.requiredScope") String requiredScope) {
        this.requiredScope = requiredScope;
    }

    @Override
    public CompletionStage<HttpServerResponse> intercept(Context context, 
                                                         HttpServerRequest request, 
                                                         InterceptChain chain) {
        Principal principal = Principal.current(context);
        if (principal instanceof PrincipalWithScopes scoped) {
            if (!scoped.scopes().contains(requiredScope)) {
                return CompletableFuture.failedFuture(
                    HttpServerResponseException.of(403, "Insufficient scope")
                );
            }
        } else {
            return CompletableFuture.failedFuture(
                HttpServerResponseException.of(403, "No scopes available")
            );
        }

        return chain.process(context, request);
    }
}
@Component
class ScopeCheckingInterceptor(
    @ConfigSource("auth.requiredScope") private val requiredScope: String
) : HttpServerInterceptor {

    override fun intercept(
        context: Context,
        request: HttpServerRequest,
        chain: HttpServerInterceptor.InterceptChain
    ): CompletionStage<HttpServerResponse> {
        val principal = Principal.current(context)
        if (principal is PrincipalWithScopes) {
            if (!principal.scopes.contains(requiredScope)) {
                return CompletableFuture.failedFuture(
                    HttpServerResponseException.of(403, "Insufficient scope")
                )
            }
        } else {
            return CompletableFuture.failedFuture(
                HttpServerResponseException.of(403, "No scopes available")
            )
        }

        return chain.process(context, request)
    }
}

OpenAPI Integration

When using Kora OpenAPI Generator, authorization is configured automatically based on the OpenAPI specification. The generator creates:

  1. ApiSecurity interface with marker classes for each authorization type
  2. HttpServerInterceptor for each security scheme
  3. Requires providing an HttpServerPrincipalExtractor with the corresponding @Tag

Example from kora-examples:

@KoraApp
public interface Application extends
        HoconConfigModule,
        UndertowHttpServerModule,
        JsonModule {

    @Tag(ApiSecurity.ApiKeyAuth.class)
    default HttpServerPrincipalExtractor<Principal> apiKeyExtractor(DataApiAuthConfig config) {
        return (request, value) -> {
            if (value == null || !config.value().equals(value)) {
                throw new SecurityException("Invalid API key");
            }
            return CompletableFuture.completedFuture(
                new DataApiPrincipal("data-api-client")
            );
        };
    }
}

where DataApiPrincipal:

public record DataApiPrincipal(String name) implements Principal {}
@KoraApp
interface Application :
    HoconConfigModule,
    UndertowHttpServerModule,
    JsonModule {

    @Tag(ApiSecurity.ApiKeyAuth::class)
    fun apiKeyExtractor(config: DataApiAuthConfig): HttpServerPrincipalExtractor<Principal> {
        return HttpServerPrincipalExtractor { request, value ->
            if (value == null || config.value() != value) {
                throw SecurityException("Invalid API key")
            }
            CompletableFuture.completedFuture(
                DataApiPrincipal("data-api-client")
            )
        }
    }
}

where DataApiPrincipal:

data class DataApiPrincipal(val name: String) : Principal

Configuration:

auth.apiKey {
  value = "secret-api-key-123"
}

Error Handling

If HttpServerPrincipalExtractor throws an exception or returns null, the request is rejected with 403 Forbidden. For custom authorization error handling, use an interceptor:

@Tag(HttpServerModule.class)
@Component
public final class AuthErrorInterceptor implements HttpServerInterceptor {

    @Override
    public CompletionStage<HttpServerResponse> intercept(Context context, 
                                                         HttpServerRequest request, 
                                                         InterceptChain chain) {
        return chain.process(context, request).exceptionally(e -> {
            if (e instanceof CompletionException) {
                e = e.getCause();
            }
            if (e instanceof IllegalAccessException) {
                return HttpServerResponse.of(401, HttpBody.plaintext("Unauthorized: " + e.getMessage()));
            }
            if (e instanceof SecurityException) {
                return HttpServerResponse.of(403, HttpBody.plaintext("Forbidden: " + e.getMessage()));
            }
            throw new CompletionException(e);
        });
    }
}
@Tag(HttpServerModule::class)
@Component
class AuthErrorInterceptor : HttpServerInterceptor {

    override fun intercept(
        context: Context,
        request: HttpServerRequest,
        chain: HttpServerInterceptor.InterceptChain
    ): CompletionStage<HttpServerResponse> {
        return chain.process(context, request).exceptionally { e ->
            val error = if (e is CompletionException) e.cause!! else e
            when (error) {
                is IllegalAccessException -> 
                    HttpServerResponse.of(401, HttpBody.plaintext("Unauthorized: ${error.message}"))
                is SecurityException -> 
                    HttpServerResponse.of(403, HttpBody.plaintext("Forbidden: ${error.message}"))
                else -> throw CompletionException(error)
            }
        }
    }
}

Telemetry

HTTP Server uses a telemetry contract for logging, metrics, and tracing of requests. Telemetry configuration (section telemetry { logging / metrics / tracing }) is described in the Configuration section. Extension points are located in ru.tinkoff.kora.http.server.common.telemetry.

For each HTTP request, an HttpServerTelemetry.HttpServerTelemetryContext is created, which is closed upon request completion. The request is described through telemetry handler parameters, including method, path, response status, and duration.

The default factory DefaultHttpServerTelemetryFactory combines three factories: - HttpServerLoggerFactory builds HttpServerLogger for logging request start/end; - HttpServerMetricsFactory builds HttpServerMetrics for writing request metrics; - HttpServerTracerFactory builds HttpServerTracer for distributed tracing.

Metrics and tracing are described in the Metrics Reference section.

Logging

Server logging is written through SLF4J under the ru.tinkoff.kora.http.server.common.HttpServer logger. Enabling logging in the configuration (httpServer.telemetry.logging.enabled = true) turns the telemetry on, but what is actually written is governed by the log level of that logger, so you control verbosity from your logging framework (logback, etc.) without restarting with a different config:

Log level What is logged
INFO Request start and end line: method, path template, response status, result code, and duration
DEBUG Additionally request and response headers
TRACE Additionally the full (non-templated) request path

The following configuration fields shape the output (see Configuration for the full list):

  • pathTemplate — when true (default), the low-cardinality route template (/users/{id}) is logged and used as the metric/trace label instead of the resolved path (/users/42); at TRACE the resolved path is logged
  • maskHeaders — header names whose values are replaced with mask (default masks authorization, cookie, set-cookie)
  • maskQueries — query parameter names whose values are replaced with mask
  • mask — the replacement string (default ***)
  • stacktrace — when true (default), logs the exception stack trace when a request fails

Example logback configuration that enables full header logging for the server:

<logger name="ru.tinkoff.kora.http.server.common.HttpServer" level="DEBUG"/>

Custom logger

To fully control the log format or destination, provide your own HttpServerLoggerFactory (or HttpServerLogger) component — it replaces the default Slf4jHttpServerLoggerFactory. The same applies to metrics (HttpServerMetricsFactory) and tracing (HttpServerTracerFactory): supplying any of these components overrides the corresponding default, while the others keep their default implementation.

@Component
public final class MyHttpServerLoggerFactory implements HttpServerLoggerFactory {

    @Override
    public HttpServerLogger get(HttpServerLoggerConfig logging) {
        return new MyHttpServerLogger(); //(1)!
    }
}
  1. Your HttpServerLogger implementation controlling exactly what and how to log
@Component
class MyHttpServerLoggerFactory : HttpServerLoggerFactory {

    override fun get(logging: HttpServerLoggerConfig): HttpServerLogger {
        return MyHttpServerLogger() //(1)!
    }
}
  1. Your HttpServerLogger implementation controlling exactly what and how to log