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¶
Implementation based on Undertow.
Dependency build.gradle
:
Module:
Dependency build.gradle.kts
:
Module:
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)!
telemetry {
logging {
enabled = false //(10)!
stacktrace = true //(11)!
mask = "***" //(12)!
maskQueries = [ ] //(13)!
maskHeaders = [ "authorization" ] //(14)!
pathTemplate = true //(15)!
}
metrics {
enabled = true //(16)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(17)!
}
tracing {
enabled = true //(18)!
}
}
}
- Public server port
- Private server port
- Path to get metrics on the private server
- Path to get probes status on the private server
- Path to get probes viability status on a private server
- 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 - Number of server threads, default is the number of CPU cores or minimum
2
. - Number of blocking threads, default is the number of CPU cores multiplied by 2 or a minimum of
2
threads. - Waiting time to shut down the server in case of normal termination
- Enables module logging (default
false
) - Enables call stack logging in case of exception
- Mask that is used to hide specified headers and request/response parameters
- List of request parameters to be hidden
- List of request/response headers that should be hidden
- Whether to always use the request path template when logging. Default is to always use the path template, except for the
TRACE
logging level, which uses the full path. - Enables module metrics (default
true
) - Configures SLO for DistributionSummary metrics
- Enables module tracing (default is
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: "30s" #(9)!
telemetry:
logging:
enabled: false #(10)!
stacktrace: true #(11)!
mask: "***" #(12)!
maskQueries: [ ] #(13)!
maskHeaders: [ "authorization" ] #(14)!
pathTemplate: true #(15)!
metrics:
enabled: true #(16)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(17)!
telemetry:
enabled: true #(18)!
- Public server port
- Private server port
- Path to get metrics on the private server
- Path to get probes status on the private server
- Path to get probes viability status on a private server
- 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 - Number of server threads, default is the number of CPU cores or minimum
2
. - Number of blocking threads, default is the number of CPU cores multiplied by 2 or a minimum of
2
threads. - Waiting time to shut down the server in case of normal termination
- Enables module logging (default
false
) - Enables call stack logging in case of exception
- Mask that is used to hide specified headers and request/response parameters
- List of request parameters to be hidden
- List of request/response headers that should be hidden
- Whether to always use the request path template when logging. The default is to always use the path template, except for the
TRACE
logging level, which uses the full path. - Enables module metrics (default
true
) - Configures SLO for DistributionSummary metrics
- Enables module tracing (default is
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";
}
}
- Indicates that the class is a component and should be registered in the application dependency container
- Indicates that the class is a controller and contains HTTP handlers
- Indicates that the method is a path handler in the controller
- Indicates the type of HTTP method handler
- 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"
}
}
- Indicates that the class is a component and should be registered in the application dependency container
- Indicates that the class is a controller and contains HTTP handlers
- Indicates that the method is a path handler in the controller
- Indicates the type of HTTP method handler
- 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.
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.
Request header¶
@Header
- value of request header, the parameter name is specified in value
or defaults to the method argument name.
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:
Need to connect Json module.
Form UrlEncoded¶
You can use FormUrlEncoded
as the body argument type and it will be processed as form data.
Form Multipart¶
You can use FormMultipart
as the body argument type and it will be treated as a binary form.
Cookie¶
@Cookie
- Cookie value, the parameter name is specified in value
or defaults to the method argument name.
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";
}
}
- Any
@Nullable
annotation will do, such asjavax.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:
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)!
);
}
}
- HTTP status response code
- Response headers
- 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)!
)
}
}
- HTTP status response code
- Response headers
- 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");
}
}
- 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.
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.
T myMethod()
CompletionStage<T> myMethod()
CompletionStageMono<T> myMethod()
Project Reactor (require dependency)
By T
we mean the type of the return value.
myMethod(): T
suspend myMethod(): T
Kotlin Coroutine (require dependency asimplementation
)
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")));
});
}
}
- Specifies the HTTP method type of the handler method
- 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")))
}
}
}
- Specifies the HTTP method type of the handler method
- Indicates the path of the handler method