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

Kora is a framework for writing Java / Kotlin applications with a focus on performance, efficiency, transparency made by T-Bank / Tinkoff developers

Skip to content

HTTP server

Module provides a thin layer of abstraction over HTTP server libraries to create HTTP request handlers using both declarative-style annotations and imperative-style annotations.

Dependency

An implementation based on Undertow is available for now. The server supports Stateful Termination.

Dependency build.gradle:

annotationProcessor "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:http-server-undertow"

Module:

@KoraApp
public interface Application extends UndertowHttpServerModule { }

Dependency build.gradle.kts:

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

Module:

@KoraApp
interface Application : UndertowHttpServerModule

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 = "100ms" //(9)!
    telemetry {
        logging {
            enabled = false //(10)!
            stacktrace = true //(11)!
        }
        metrics {
            enabled = true //(12)!
            slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(13)!
        }
        tracing {
            enabled = true //(14)!
        }
    }
}
  1. Public server port
  2. Private server port
  3. Path to get metrics on the private server
  4. Path to get probes status on the private server
  5. Path to get probes viability status on a private server
  6. Whether to ignore the slash at the end of the path, if enabled /my/path and /my/path/ will be interpreted the same way, default is off
  7. Number of server threads, default is the number of CPU cores or minimum 2.
  8. Number of blocking threads, default is the number of CPU cores multiplied by 2 or a minimum of 2 threads.
  9. Waiting time to shut down the server in case of normal termination
  10. Enables module logging (default false)
  11. Enables call stack logging in case of exception
  12. Enables module metrics (default true)
  13. Configures SLO for DistributionSummary metrics
  14. Enables module tracing (default true)
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: "100ms" #(9)!
  telemetry:
    logging:
      enabled: false #(10)!
      stacktrace: true #(11)!
    metrics:
      enabled: true #(12)!
      slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(13)!
    telemetry:
      enabled: true #(14)!
  1. Public server port
  2. Private server port
  3. Path to get metrics on the private server
  4. Path to get probes status on the private server
  5. Path to get probes viability status on a private server
  6. Whether to ignore the slash at the end of the path, if enabled /my/path and /my/path/ will be interpreted the same way, default is off
  7. Number of server threads, default is the number of CPU cores or minimum 2.
  8. Number of blocking threads, default is the number of CPU cores multiplied by 2 or a minimum of 2 threads.
  9. Waiting time to shut down the server in case of normal termination
  10. Enables module logging (default false)
  11. Enables call stack logging in case of exception
  12. Enables module metrics (default true)
  13. Configures SLO for DistributionSummary metrics
  14. Enables module tracing (default true)

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 HTTP method handler
  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 HTTP method handler
  5. Indicates the path of the handler method

Request

The section describes HTTP request transformations at the controller. It is suggested to use special annotations to specify the request parameters.

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.

@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.

@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.

@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 body of a request requires using a method argument without special annotations, default supported types are byte[], ByteBuffer, String.

Json

In order to indicate that the body is Json and needs to automatically create such a reader and embed it, is required to use the @Json annotation:

@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 written 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 written as Json

Need to connect Json module.

Form UrlEncoded

You can use FormUrlEncoded as the body argument type and it will be processed as form data.

@Component
@HttpController
public final class SomeController {

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

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun helloWorld(body: FormUrlEncoded): String {
        return "Hello World"
    }
}
Form Multipart

You can use FormMultipart as the body argument type and it will be treated as a binary form.

@Component
@HttpController
public final class SomeController {

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

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

@Cookie - Cookie value, the parameter name is specified in value or defaults to the method argument name.

@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

In case you need to handle the request in a different way, you can use a special HttpServerRequestMapper interface:

@Component
@HttpController
public final class SomeController {

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

    public static final class RequestMapper implements HttpServerRequestMapper<UserContext> {

        @Override
        public UserContext apply(HttpServerRequest request) {
            return new UserContext(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) UserContext context) {
        return "Hello World";
    }
}
@Component
@HttpController
class MapperRequestController {

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

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

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

Required parameters

By default, all arguments declared in a method are required (NotNull).

By default, all arguments declared in a method that do not use the Kotlin Nullability syntax are required (NotNull).

Optional parameters

In case a method argument is optional, that is, it may not exist then, @Nullable annotation can be used:

@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, such as javax.annotation.Nullable / jakarta.annotation.Nullable / org.jetbrains.annotations.Nullable / etc.

It is expected to use the Kotlin Nullability syntax and mark such a parameter as Nullable:

@Component
@HttpController
class SomeController {

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

Response

By default, you can use standard return value types, such as byte[], ByteBuffer, String which will be processed with status code 200 and corresponding response type header or HttpServerResponse where you will have to fill in all information about HTTP response yourself.

@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(body) //(3)!
        ); 
    }
}
  1. HTTP status response 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(body) //(3)!
        )
    }
}
  1. HTTP status response code
  2. Response headers
  3. Response body

Json

If you intend to respond in Json format, you are required to use the @Json annotation over the method:

@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

Json module is required.

Response entity

If the intention is to read the body and also get the headers and status code of the response, then the HttpResponseEntity is supposed to be used, it is a wrapper over the response body.

Below is an example similar to the Json example along 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 you need to respond with an error, you can use HttpServerResponseException to throw an exception.

@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

In case you need to read the response in a different way, you can use the special HttpServerResponseMapper interface:

@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> {
        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 repository methods out of the box:

The T refers to the type of the return value.

By T we mean the type of the return value.

Interceptors

You can create interceptors to change behavior or create additional behavior using the HttpServerInterceptor class. Interceptors can be overlaid:

  • On specific methods of the controller
  • On the whole controller class
  • On all controller classes simultaneously (requires using @Tag(HttpServerModule.class) over the interceptor class).
@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).exceptionally(e -> {
                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);
                }
            });
        }
    }

    @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).exceptionally { e ->
                val body = HttpBody.plaintext(e.message)
                when (e) {
                    is HttpServerResponseException -> e
                    is IllegalArgumentException -> HttpServerResponse.of(400, body)
                    is TimeoutException -> HttpServerResponse.of(408, body)
                    else -> HttpServerResponse.of(500, body)
                }
            }
        }
    }

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

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