From dabf1e6f2e55f1ed8099a7ac7be300ec9a400cb0 Mon Sep 17 00:00:00 2001 From: ayazychyan Date: Thu, 26 Feb 2026 20:52:27 +0300 Subject: [PATCH] gh-362: internal http server api --- .../http/server/common/HttpServerModule.java | 19 +- .../common/InternalHttpServerConfig.java | 12 ++ .../http/server/common/NoopHttpServer.java | 21 +++ .../server/common/annotation/InternalApi.java | 10 + .../http/server/common/HttpServerTestKit.java | 175 +++++++++++++++--- .../undertow/UndertowHttpServerModule.java | 22 +++ .../http/server/undertow/UndertowModule.java | 1 + 7 files changed, 237 insertions(+), 23 deletions(-) create mode 100644 http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/InternalHttpServerConfig.java create mode 100644 http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/NoopHttpServer.java create mode 100644 http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/annotation/InternalApi.java diff --git a/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/HttpServerModule.java b/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/HttpServerModule.java index ef8022dc4..29f4e282e 100644 --- a/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/HttpServerModule.java +++ b/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/HttpServerModule.java @@ -2,6 +2,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.opentelemetry.api.trace.Tracer; +import java.util.Optional; import org.jspecify.annotations.Nullable; import ru.tinkoff.kora.application.graph.All; import ru.tinkoff.kora.application.graph.PromiseOf; @@ -13,6 +14,7 @@ import ru.tinkoff.kora.config.common.Config; import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractionException; import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractor; +import ru.tinkoff.kora.http.server.common.annotation.InternalApi; import ru.tinkoff.kora.http.server.common.annotation.PrivateApi; import ru.tinkoff.kora.http.server.common.handler.HttpServerRequestHandler; import ru.tinkoff.kora.http.server.common.privateapi.LivenessHandler; @@ -23,8 +25,6 @@ import ru.tinkoff.kora.http.server.common.telemetry.impl.DefaultHttpServerTelemetryFactory; import ru.tinkoff.kora.telemetry.common.MetricsScraper; -import java.util.Optional; - public interface HttpServerModule extends StringParameterReadersModule, HttpServerRequestMapperModule, HttpServerResponseMapperModule { default HttpServerConfig httpServerConfig(Config config, ConfigValueExtractor configValueExtractor) { @@ -77,4 +77,19 @@ default HttpServerHandler privateApiHandler(@Tag(PrivateApi.class) All configValueExtractor) { + var value = config.get("internalHttpServer"); + var parsed = configValueExtractor.extract(value); + if (parsed == null) { + throw ConfigValueExtractionException.missingValueAfterParse(value); + } + return parsed; + } + + @InternalApi + default HttpServerHandler internalApiHandler(@Tag(InternalApi.class) All handlers, @Tag(InternalApi.class) All interceptors, HttpServerConfig config) { + return new HttpServerHandler(handlers, interceptors, config); + } + } diff --git a/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/InternalHttpServerConfig.java b/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/InternalHttpServerConfig.java new file mode 100644 index 000000000..c94d20259 --- /dev/null +++ b/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/InternalHttpServerConfig.java @@ -0,0 +1,12 @@ +package ru.tinkoff.kora.http.server.common; + +import ru.tinkoff.kora.config.common.annotation.ConfigValueExtractor; + +@ConfigValueExtractor +public interface InternalHttpServerConfig extends HttpServerConfig { + + @Override + default int port() { + return 8090; + } +} diff --git a/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/NoopHttpServer.java b/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/NoopHttpServer.java new file mode 100644 index 000000000..d2230c52e --- /dev/null +++ b/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/NoopHttpServer.java @@ -0,0 +1,21 @@ +package ru.tinkoff.kora.http.server.common; + +public enum NoopHttpServer implements HttpServer { + + INSTANCE; + + @Override + public int port() { + return -1; + } + + @Override + public void init() { + // no-op + } + + @Override + public void release() { + // no-op + } +} diff --git a/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/annotation/InternalApi.java b/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/annotation/InternalApi.java new file mode 100644 index 000000000..213d10851 --- /dev/null +++ b/http/http-server-common/src/main/java/ru/tinkoff/kora/http/server/common/annotation/InternalApi.java @@ -0,0 +1,10 @@ +package ru.tinkoff.kora.http.server.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import ru.tinkoff.kora.common.Tag; + +@Tag(InternalApi.class) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) +public @interface InternalApi { +} diff --git a/http/http-server-common/src/testFixtures/java/ru/tinkoff/kora/http/server/common/HttpServerTestKit.java b/http/http-server-common/src/testFixtures/java/ru/tinkoff/kora/http/server/common/HttpServerTestKit.java index 40654de50..1bf585961 100644 --- a/http/http-server-common/src/testFixtures/java/ru/tinkoff/kora/http/server/common/HttpServerTestKit.java +++ b/http/http-server-common/src/testFixtures/java/ru/tinkoff/kora/http/server/common/HttpServerTestKit.java @@ -1,6 +1,33 @@ package ru.tinkoff.kora.http.server.common; +import static java.time.Instant.now; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static ru.tinkoff.kora.http.common.HttpMethod.GET; +import static ru.tinkoff.kora.http.common.HttpMethod.POST; + import io.opentelemetry.api.trace.Span; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -8,7 +35,11 @@ import okio.BufferedSink; import org.jetbrains.annotations.NotNull; import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.mockito.AdditionalAnswers; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -32,28 +63,15 @@ import ru.tinkoff.kora.http.server.common.privateapi.MetricsHandler; import ru.tinkoff.kora.http.server.common.privateapi.ReadinessHandler; import ru.tinkoff.kora.http.server.common.router.HttpServerHandler; -import ru.tinkoff.kora.http.server.common.telemetry.*; +import ru.tinkoff.kora.http.server.common.telemetry.$HttpServerTelemetryConfig_ConfigValueExtractor; +import ru.tinkoff.kora.http.server.common.telemetry.$HttpServerTelemetryConfig_HttpServerLoggingConfig_ConfigValueExtractor; +import ru.tinkoff.kora.http.server.common.telemetry.$HttpServerTelemetryConfig_HttpServerMetricsConfig_ConfigValueExtractor; +import ru.tinkoff.kora.http.server.common.telemetry.$HttpServerTelemetryConfig_HttpServerTracingConfig_ConfigValueExtractor; +import ru.tinkoff.kora.http.server.common.telemetry.HttpServerObservation; +import ru.tinkoff.kora.http.server.common.telemetry.HttpServerTelemetry; +import ru.tinkoff.kora.http.server.common.telemetry.NoopHttpServerTelemetry; import ru.tinkoff.kora.telemetry.common.MetricsScraper; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.*; -import java.util.function.Supplier; - -import static java.time.Instant.now; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.Mockito.*; -import static ru.tinkoff.kora.http.common.HttpMethod.GET; -import static ru.tinkoff.kora.http.common.HttpMethod.POST; - @TestInstance(TestInstance.Lifecycle.PER_METHOD) public abstract class HttpServerTestKit { protected static MetricsScraper registry = Mockito.mock(MetricsScraper.class); @@ -74,6 +92,7 @@ public abstract class HttpServerTestKit { private volatile HttpServer httpServer = null; private volatile HttpServer privateHttpServer = null; + private volatile HttpServer internalHttpServer = null; protected final OkHttpClient client = new OkHttpClient.Builder() .connectionPool(new ConnectionPool(0, 1, TimeUnit.MICROSECONDS)) @@ -190,6 +209,84 @@ void testReadinessFailureOnUninitializedProbe() throws IOException { } + @Nested + public class InternalApiTest { + @Test + void testNoopHttpServerWhenNoHandlers() { + var noopServer = NoopHttpServer.INSTANCE; + assertThat(noopServer.port()).isEqualTo(-1); + Assertions.assertDoesNotThrow(noopServer::init); + Assertions.assertDoesNotThrow(noopServer::release); + } + + @Test + void testInternalApiHelloWorld() throws IOException { + var httpResponse = HttpServerResponse.of(200, HttpBody.plaintext("internal hello")); + var handler = handler(GET, "/internal", (_) -> httpResponse); + startInternalHttpServer(handler); + + var request = internalApiRequest("/internal") + .get() + .build(); + + try (var response = client.newCall(request).execute()) { + assertThat(response.code()).isEqualTo(200); + assertThat(response.body().string()).isEqualTo("internal hello"); + } + } + + @Test + void testInternalApiUnknownPath() throws IOException { + var handler = handler(GET, "/internal", (_) -> HttpServerResponse.of(200)); + startInternalHttpServer(handler); + + var request = internalApiRequest("/unknown") + .get() + .build(); + + try (var response = client.newCall(request).execute()) { + assertThat(response.code()).isEqualTo(404); + } + } + + @Test + void testInternalApiWithInterceptor() throws IOException { + var httpResponse = HttpServerResponse.of(200, HttpBody.plaintext("internal hello")); + var handler = handler(GET, "/internal", (_) -> httpResponse); + var interceptor = new HttpServerInterceptor() { + @Override + public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) throws Exception { + var header = request.headers().getFirst("x-internal-block"); + if (header != null) { + request.body().close(); + return HttpServerResponse.of(403, HttpBody.plaintext("blocked")); + } + return chain.process(request); + } + }; + startInternalHttpServer(List.of(interceptor), handler); + + var request = internalApiRequest("/internal") + .get() + .build(); + + try (var response = client.newCall(request).execute()) { + assertThat(response.code()).isEqualTo(200); + assertThat(response.body().string()).isEqualTo("internal hello"); + } + + var blockedRequest = internalApiRequest("/internal") + .header("x-internal-block", "true") + .get() + .build(); + + try (var response = client.newCall(blockedRequest).execute()) { + assertThat(response.code()).isEqualTo(403); + assertThat(response.body().string()).isEqualTo("blocked"); + } + } + } + @Nested public class PublicApiTest { @Test @@ -990,6 +1087,34 @@ protected void startPrivateHttpServer() { } } + protected void startInternalHttpServer(HttpServerRequestHandler... handlers) { + startInternalHttpServer(List.of(), handlers); + } + + protected void startInternalHttpServer(List interceptors, HttpServerRequestHandler... handlers) { + var config = new HttpServerConfig_Impl( + 0, + false, + Duration.ofSeconds(1), + Duration.ofSeconds(1), + false, + Duration.ofMillis(1), + new $HttpServerTelemetryConfig_ConfigValueExtractor.HttpServerTelemetryConfig_Impl( + new $HttpServerTelemetryConfig_HttpServerLoggingConfig_ConfigValueExtractor.HttpServerLoggingConfig_Defaults(), + new $HttpServerTelemetryConfig_HttpServerMetricsConfig_ConfigValueExtractor.HttpServerMetricsConfig_Defaults(), + new $HttpServerTelemetryConfig_HttpServerTracingConfig_ConfigValueExtractor.HttpServerTracingConfig_Defaults() + ), + Size.of(1, Size.Type.GiB) + ); + var internalApiHandler = new HttpServerHandler(List.of(handlers), interceptors, config); + this.internalHttpServer = this.httpServer(valueOf(config), internalApiHandler, this.telemetry); + try { + this.internalHttpServer.init(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @AfterEach void tearDown() throws Exception { if (this.httpServer != null) { @@ -1000,6 +1125,10 @@ void tearDown() throws Exception { this.privateHttpServer.release(); this.privateHttpServer = null; } + if (this.internalHttpServer != null) { + this.internalHttpServer.release(); + this.internalHttpServer = null; + } this.readinessProbePromise.setValue(readinessProbe); this.livenessProbePromise.setValue(livenessProbe); } @@ -1021,6 +1150,10 @@ protected Request.Builder privateApiRequest(String path) { return request(this.privateHttpServer.port(), path); } + protected Request.Builder internalApiRequest(String path) { + return request(this.internalHttpServer.port(), path); + } + protected Request.Builder request(String path) { return request(this.httpServer.port(), path); } diff --git a/http/http-server-undertow/src/main/java/ru/tinkoff/kora/http/server/undertow/UndertowHttpServerModule.java b/http/http-server-undertow/src/main/java/ru/tinkoff/kora/http/server/undertow/UndertowHttpServerModule.java index b983ad39a..bc5b78268 100644 --- a/http/http-server-undertow/src/main/java/ru/tinkoff/kora/http/server/undertow/UndertowHttpServerModule.java +++ b/http/http-server-undertow/src/main/java/ru/tinkoff/kora/http/server/undertow/UndertowHttpServerModule.java @@ -3,10 +3,17 @@ import io.undertow.Undertow; import org.jspecify.annotations.Nullable; import org.xnio.XnioWorker; +import ru.tinkoff.kora.application.graph.All; import ru.tinkoff.kora.application.graph.ValueOf; +import ru.tinkoff.kora.common.Tag; import ru.tinkoff.kora.common.annotation.Root; import ru.tinkoff.kora.common.util.Configurer; +import ru.tinkoff.kora.http.server.common.HttpServer; import ru.tinkoff.kora.http.server.common.HttpServerConfig; +import ru.tinkoff.kora.http.server.common.InternalHttpServerConfig; +import ru.tinkoff.kora.http.server.common.NoopHttpServer; +import ru.tinkoff.kora.http.server.common.annotation.InternalApi; +import ru.tinkoff.kora.http.server.common.handler.HttpServerRequestHandler; import ru.tinkoff.kora.http.server.common.router.HttpServerHandler; import ru.tinkoff.kora.http.server.common.telemetry.HttpServerTelemetryFactory; @@ -20,4 +27,19 @@ default UndertowHttpServer undertowHttpServer(ValueOf config, var telemetry = telemetryFactory.get(config.get().telemetry()); return new UndertowHttpServer(config, handler, "kora-undertow", telemetry, worker, configurer); } + + @Root + @InternalApi + default HttpServer internalApiUndertowHttpServer(@Tag(InternalApi.class) All handlers, + @InternalApi ValueOf config, + @InternalApi ValueOf handler, + HttpServerTelemetryFactory telemetryFactory, + XnioWorker worker, + @Nullable @InternalApi Configurer configurer) { + if (handlers.isEmpty()) { + return NoopHttpServer.INSTANCE; + } + var telemetry = telemetryFactory.get(config.get().telemetry()); + return new UndertowHttpServer(config, handler, "kora-undertow-internal", telemetry, worker, configurer); + } } diff --git a/http/http-server-undertow/src/main/java/ru/tinkoff/kora/http/server/undertow/UndertowModule.java b/http/http-server-undertow/src/main/java/ru/tinkoff/kora/http/server/undertow/UndertowModule.java index 62d677628..379018f0f 100644 --- a/http/http-server-undertow/src/main/java/ru/tinkoff/kora/http/server/undertow/UndertowModule.java +++ b/http/http-server-undertow/src/main/java/ru/tinkoff/kora/http/server/undertow/UndertowModule.java @@ -38,4 +38,5 @@ default UndertowConfig undertowHttpServerConfig(Config config, ConfigValueExtrac } return parsed; } + }