From 79299618dee044d36904f982d42cc1a21708bff2 Mon Sep 17 00:00:00 2001 From: artem-v Date: Mon, 17 Nov 2025 20:05:38 +0200 Subject: [PATCH 01/17] WIP on HttpGatewayClientTransport --- .../http/HttpGatewayClientTransport.java | 90 +++++++++++++++---- .../services}/ServiceRegistrationTest.java | 17 ++-- 2 files changed, 80 insertions(+), 27 deletions(-) rename {services-gateway/src/test/java/io/scalecube/services/gateway/rest => services/src/test/java/io/scalecube/services}/ServiceRegistrationTest.java (94%) diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java index f128cd556..01576588a 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java @@ -1,10 +1,16 @@ package io.scalecube.services.gateway.client.http; +import static io.netty.handler.codec.http.HttpMethod.DELETE; +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpMethod.HEAD; +import static io.netty.handler.codec.http.HttpMethod.OPTIONS; +import static io.netty.handler.codec.http.HttpMethod.TRACE; import static io.scalecube.services.gateway.client.ServiceMessageCodec.decodeData; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelOption; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.scalecube.services.Address; import io.scalecube.services.ServiceReference; @@ -14,14 +20,17 @@ import io.scalecube.services.transport.api.ClientTransport; import io.scalecube.services.transport.api.DataCodec; import java.lang.reflect.Type; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.UnaryOperator; +import java.util.stream.Collectors; import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.NettyOutbound; @@ -33,12 +42,11 @@ public final class HttpGatewayClientTransport implements ClientChannel, ClientTransport { - private static final Logger LOGGER = LoggerFactory.getLogger(HttpGatewayClientTransport.class); - private static final String CONTENT_TYPE = "application/json"; private static final HttpGatewayClientCodec CLIENT_CODEC = new HttpGatewayClientCodec(DataCodec.getInstance(CONTENT_TYPE)); private static final int CONNECT_TIMEOUT_MILLIS = (int) Duration.ofSeconds(5).toMillis(); + private static final Set BODYLESS_METHODS = Set.of(GET, HEAD, DELETE, OPTIONS, TRACE); private final GatewayClientCodec clientCodec; private final LoopResources loopResources; @@ -80,14 +88,17 @@ public ClientChannel create(ServiceReference serviceReference) { } @Override - public Mono requestResponse(ServiceMessage request, Type responseType) { + public Mono requestResponse(ServiceMessage message, Type responseType) { return Mono.defer( () -> { - final HttpClient httpClient = httpClientReference.get(); + final var httpClient = httpClientReference.get(); + final var method = message.headers().getOrDefault("http.method", "POST"); + final var queryParams = extractPrefix(message.headers(), "http.query."); + return httpClient - .post() - .uri("/" + request.qualifier()) - .send((clientRequest, outbound) -> send(request, clientRequest, outbound)) + .request(HttpMethod.valueOf(method)) + .uri(applyQueryParams("/" + message.qualifier(), queryParams)) + .send((request, outbound) -> send(message, request, outbound)) .responseSingle( (clientResponse, mono) -> mono.map(ByteBuf::retain).map(data -> toMessage(clientResponse, data))) @@ -96,12 +107,24 @@ public Mono requestResponse(ServiceMessage request, Type respons } private Mono send( - ServiceMessage request, HttpClientRequest clientRequest, NettyOutbound outbound) { - LOGGER.debug("Sending request: {}", request); - // prepare request headers - request.headers().forEach(clientRequest::header); - // send with publisher (defer buffer cleanup to netty) - return outbound.sendObject(Mono.just(clientCodec.encode(request))).then(); + ServiceMessage message, HttpClientRequest request, NettyOutbound outbound) { + // Extract custom headers + final var messageHeaders = message.headers(); + final var httpHeaders = extractPrefix(messageHeaders, "http.header."); + + // Apply HTTP headers first + httpHeaders.forEach(request::header); + + // Apply remaining message headers (skip http.*) + messageHeaders.entrySet().stream() + .filter(e -> !e.getKey().startsWith("http.")) + .forEach(e -> request.header(e.getKey(), e.getValue())); + + if (BODYLESS_METHODS.contains(request.method())) { + return outbound.then(); + } + + return outbound.sendObject(Mono.just(clientCodec.encode(message))).then(); } @Override @@ -129,16 +152,47 @@ private static ServiceMessage toMessage(HttpClientResponse httpResponse, ByteBuf .responseHeaders() .entries() .forEach(entry -> builder.header(entry.getKey(), entry.getValue())); - ServiceMessage message = builder.build(); - LOGGER.debug("Received response: {}", message); - return message; + return builder.build(); } private static boolean isError(HttpResponseStatus status) { return status.code() >= 400 && status.code() <= 599; } + private static String applyQueryParams(String uri, Map queryParams) { + if (queryParams != null && !queryParams.isEmpty()) { + final var queryString = + queryParams.entrySet().stream() + .map( + e -> { + final var key = e.getKey(); + final var value = e.getValue(); + final var charset = StandardCharsets.UTF_8; + return URLEncoder.encode(key, charset) + + "=" + + URLEncoder.encode(value, charset); + }) + .collect(Collectors.joining("&")); + uri += "?" + queryString; + } + return uri; + } + + private static Map extractPrefix(Map headers, String prefix) { + if (headers == null || headers.isEmpty()) { + return Map.of(); + } + final var result = new HashMap(); + headers.forEach( + (k, v) -> { + if (k.startsWith(prefix)) { + result.put(k.substring(prefix.length()), v); + } + }); + return result; + } + @Override public void close() { if (ownsLoopResources) { diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/ServiceRegistrationTest.java b/services/src/test/java/io/scalecube/services/ServiceRegistrationTest.java similarity index 94% rename from services-gateway/src/test/java/io/scalecube/services/gateway/rest/ServiceRegistrationTest.java rename to services/src/test/java/io/scalecube/services/ServiceRegistrationTest.java index 379e99ce4..bbf4ec850 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/ServiceRegistrationTest.java +++ b/services/src/test/java/io/scalecube/services/ServiceRegistrationTest.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.rest; +package io.scalecube.services; import static io.scalecube.services.api.ServiceMessage.HEADER_REQUEST_METHOD; import static org.hamcrest.MatcherAssert.assertThat; @@ -8,7 +8,6 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; -import io.scalecube.services.Microservices; import io.scalecube.services.Microservices.Context; import io.scalecube.services.annotations.RestMethod; import io.scalecube.services.annotations.Service; @@ -117,7 +116,7 @@ void registerMultipleValidRestServices() { interface EchoService { @ServiceMethod("get/:foo") - Mono echo(); + Mono echo(); } @Service("v1/service") @@ -125,11 +124,11 @@ interface BadRestService { @RestMethod("GET") @ServiceMethod("get/:foo") - Mono echo(); + Mono echo(); @RestMethod("GET") @ServiceMethod("get/:foo") - Mono ping(); + Mono ping(); } @Service("v1/service") @@ -137,11 +136,11 @@ interface GoodRestService { @RestMethod("GET") @ServiceMethod("echo/:foo") - Mono echo(); + Mono echo(); @RestMethod("POST") @ServiceMethod("echo/:foo") - Mono ping(); + Mono ping(); } @Service("v1/service") @@ -149,7 +148,7 @@ interface CreateRestService { @RestMethod("POST") @ServiceMethod("account/:foo") - Mono account(); + Mono account(); } @Service("v1/service") @@ -157,6 +156,6 @@ interface UpdateRestService { @RestMethod("PUT") @ServiceMethod("account/:foo") - Mono account(); + Mono account(); } } From a5f976c94682c473028969ba60f8e3ba56f82104 Mon Sep 17 00:00:00 2001 From: artem-v Date: Mon, 17 Nov 2025 21:14:51 +0200 Subject: [PATCH 02/17] WIP ... --- .../gateway/rest/RestGatewayTest.java | 396 +++++++++++------- .../services/gateway/rest/RestService.java | 2 +- .../gateway/rest/RestServiceImpl.java | 7 +- 3 files changed, 241 insertions(+), 164 deletions(-) diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java index 6b0d3f9c6..4b85cf9bc 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java @@ -1,50 +1,51 @@ package io.scalecube.services.gateway.rest; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import io.netty.handler.codec.http.HttpResponseStatus; +import io.scalecube.services.Address; import io.scalecube.services.Microservices; import io.scalecube.services.Microservices.Context; import io.scalecube.services.ServiceCall; import io.scalecube.services.ServiceInfo; -import io.scalecube.services.api.ErrorData; +import io.scalecube.services.api.ServiceMessage; import io.scalecube.services.discovery.ScalecubeServiceDiscovery; import io.scalecube.services.examples.GreetingServiceImpl; import io.scalecube.services.exceptions.ServiceUnavailableException; +import io.scalecube.services.gateway.client.http.HttpGatewayClientTransport; import io.scalecube.services.gateway.client.websocket.WebsocketGatewayClientTransport; import io.scalecube.services.gateway.http.HttpGateway; import io.scalecube.services.gateway.websocket.WebsocketGateway; import io.scalecube.services.routing.StaticAddressRouter; import io.scalecube.services.transport.rsocket.RSocketServiceTransport; import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublishers; -import java.net.http.HttpResponse.BodyHandlers; import java.time.Duration; +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import reactor.test.StepVerifier; public class RestGatewayTest { private static Microservices gateway; private static Microservices microservices; - private static HttpClient httpClient; - private final ObjectMapper objectMapper = objectMapper(); - private static String httpGatewayAddress; + private static final ObjectMapper objectMapper = objectMapper(); + private static Address gatewayAddress; + private static StaticAddressRouter router; @BeforeAll static void beforeAll() { @@ -61,7 +62,8 @@ static void beforeAll() { .gateway(() -> HttpGateway.builder().id("HTTP").build()) .gateway(() -> WebsocketGateway.builder().id("WS").build())); - httpGatewayAddress = "http://localhost:" + gateway.gateway("HTTP").address().port(); + gatewayAddress = Address.from("localhost:" + gateway.gateway("HTTP").address().port()); + router = StaticAddressRouter.forService(gatewayAddress, "app-service").build(); microservices = Microservices.start( @@ -78,8 +80,6 @@ static void beforeAll() { .services(new GreetingServiceImpl()) .services(ServiceInfo.fromServiceInstance(new RestServiceImpl()).build()) .services(ServiceInfo.fromServiceInstance(new RoutingServiceImpl()).build())); - - httpClient = HttpClient.newHttpClient(); } @AfterAll @@ -98,201 +98,277 @@ private static ObjectMapper objectMapper() { mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setVisibility(PropertyAccessor.ALL, Visibility.ANY); + mapper.setSerializationInclusion(Include.NON_NULL); mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); mapper.registerModule(new JavaTimeModule()); return mapper; } @Nested + @TestInstance(Lifecycle.PER_CLASS) class GatewayTests { + private ServiceCall serviceCall; + + @BeforeAll + void beforeAll() { + serviceCall = + new ServiceCall() + .transport(HttpGatewayClientTransport.builder().address(gatewayAddress).build()) + .router(router); + } + + @AfterAll + void afterAll() { + if (serviceCall != null) { + serviceCall.close(); + } + } + @Test - void testOptions() throws Exception { - final var fooParam = "options" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder( - new URI(httpGatewayAddress + "/v1/restService/options/" + fooParam)) - .method("OPTIONS", BodyPublishers.noBody()) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - fooParam, - objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), - "response"); + void testOptions() { + final var param = "options" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "OPTIONS") + .qualifier("v1/restService/options/" + param) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(param, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testGet() throws Exception { - final var fooParam = "get" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/get/" + fooParam)) - .method("GET", BodyPublishers.noBody()) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - fooParam, - objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), - "response"); + void testGet() { + final var param = "get" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "GET") + .qualifier("v1/restService/get/" + param) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(param, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testHead() throws Exception { - final var fooParam = "head" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/head/" + fooParam)) - .method("HEAD", BodyPublishers.noBody()) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals("", httpResponse.body(), "response"); + void testHead() { + final var param = "head" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "HEAD") + .qualifier("v1/restService/head/" + param) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(param, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testPost() throws Exception { + void testPost() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "post" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/post/" + fooParam)) - .method( - "POST", - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - name, objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), "response"); + final var param = "post" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "POST") + .qualifier("v1/restService/post/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(name, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testPut() throws Exception { + void testPut() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "put" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/put/" + fooParam)) - .method( - "PUT", - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - name, objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), "response"); + final var param = "put" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "PUT") + .qualifier("v1/restService/put/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(name, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testPatch() throws Exception { + void testPatch() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "patch" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/patch/" + fooParam)) - .method( - "PATCH", - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - name, objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), "response"); + final var param = "patch" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "PATCH") + .qualifier("v1/restService/patch/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(name, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testDelete() throws Exception { + void testDelete() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "delete" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/delete/" + fooParam)) - .method( - "DELETE", - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - name, objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), "response"); + final var param = "delete" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "DELETE") + .qualifier("v1/restService/delete/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(param, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testTrace() throws Exception { - final var fooParam = "trace" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/trace/" + fooParam)) - .method("TRACE", BodyPublishers.noBody()) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - fooParam, - objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), - "response"); + void testTrace() { + final var param = "trace" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "TRACE") + .qualifier("v1/restService/trace/" + param) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(param, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } } @Nested + @TestInstance(Lifecycle.PER_CLASS) class RoutingTests { + private ServiceCall serviceCall; + + @BeforeAll + void beforeAll() { + serviceCall = + new ServiceCall() + .transport(HttpGatewayClientTransport.builder().address(gatewayAddress).build()) + .router(router); + } + + @AfterAll + void afterAll() { + if (serviceCall != null) { + serviceCall.close(); + } + } + @Test - void testMatchByGetMethod() throws Exception { - final var fooParam = "get" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder( - new URI(httpGatewayAddress + "/v1/routingService/find/" + fooParam)) - .method("GET", BodyPublishers.noBody()) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - fooParam, - objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), - "response"); + void testMatchByGetMethod() { + final var param = "get" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "GET") + .qualifier("v1/routingService/find/" + param) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(param, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testMatchByPostMethod() throws Exception { + void testMatchByPostMethod() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "post" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder( - new URI(httpGatewayAddress + "/v1/routingService/update/" + fooParam)) - .method( - "POST", - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - name, objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), "response"); + final var param = "post" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "POST") + .qualifier("v1/routingService/update/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(name, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testNoMatchByRestMethod() throws Exception { + void testNoMatchByRestMethod() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "post" + System.currentTimeMillis(); + final var param = "post" + System.currentTimeMillis(); final var nonMatchedRestMethod = "PUT"; - final var httpRequest = - HttpRequest.newBuilder( - new URI(httpGatewayAddress + "/v1/routingService/update/" + fooParam)) - .method( - nonMatchedRestMethod, - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals( - HttpResponseStatus.SERVICE_UNAVAILABLE.code(), httpResponse.statusCode(), "statusCode"); - assertTrue( - objectMapper - .readValue(httpResponse.body(), ErrorData.class) - .getErrorMessage() - .startsWith("No reachable member with such service")); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", nonMatchedRestMethod) + .qualifier("v1/routingService/update/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .verifyErrorSatisfies( + ex -> { + final var exception = (ServiceUnavailableException) ex; + assertEquals(503, exception.errorCode(), "errorCode"); + assertThat( + exception.getMessage(), + Matchers.startsWith("No reachable member with such service")); + }); } @Test diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java index 73bdde976..9cc2793e0 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java @@ -26,7 +26,7 @@ public interface RestService { Mono patch(SomeRequest request); @ServiceMethod("delete/:foo") - Mono delete(SomeRequest request); + Mono delete(); @ServiceMethod("trace/:foo") Mono trace(); diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java index c8eab3e46..4b44b2285 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java @@ -91,15 +91,16 @@ public Mono patch(SomeRequest request) { } @Override - public Mono delete(SomeRequest request) { + public Mono delete() { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathVar("foo")); + final var foo = context.pathVar("foo"); + assertNotNull(foo); assertNotNull(context.headers()); assertTrue(context.headers().size() > 0); assertEquals("DELETE", context.requestMethod()); - return new SomeResponse().name(request.name()); + return new SomeResponse().name(foo); }); } From 99fabc21a407ad9d577ec6de7cbe6b0c5f2ead68 Mon Sep 17 00:00:00 2001 From: artem-v Date: Mon, 17 Nov 2025 22:54:21 +0200 Subject: [PATCH 03/17] Done with HttpGatewayClientTransport --- .../http/HttpGatewayClientTransport.java | 42 +++++++++---------- .../gateway/http/HttpGatewayAcceptor.java | 6 ++- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java index 01576588a..1ffea2eb8 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java @@ -8,6 +8,7 @@ import static io.scalecube.services.gateway.client.ServiceMessageCodec.decodeData; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelOption; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMethod; @@ -93,7 +94,7 @@ public Mono requestResponse(ServiceMessage message, Type respons () -> { final var httpClient = httpClientReference.get(); final var method = message.headers().getOrDefault("http.method", "POST"); - final var queryParams = extractPrefix(message.headers(), "http.query."); + final var queryParams = headersByPrefix(message.headers(), "http.query"); return httpClient .request(HttpMethod.valueOf(method)) @@ -101,29 +102,23 @@ public Mono requestResponse(ServiceMessage message, Type respons .send((request, outbound) -> send(message, request, outbound)) .responseSingle( (clientResponse, mono) -> - mono.map(ByteBuf::retain).map(data -> toMessage(clientResponse, data))) + mono.defaultIfEmpty(Unpooled.EMPTY_BUFFER) + .map(ByteBuf::retain) + .map(data -> toMessage(clientResponse, data))) .map(msg -> decodeData(msg, responseType)); }); } private Mono send( ServiceMessage message, HttpClientRequest request, NettyOutbound outbound) { - // Extract custom headers - final var messageHeaders = message.headers(); - final var httpHeaders = extractPrefix(messageHeaders, "http.header."); - - // Apply HTTP headers first - httpHeaders.forEach(request::header); - - // Apply remaining message headers (skip http.*) - messageHeaders.entrySet().stream() - .filter(e -> !e.getKey().startsWith("http.")) - .forEach(e -> request.header(e.getKey(), e.getValue())); + // Populate HTTP request headers + message.headers().forEach(request::header); if (BODYLESS_METHODS.contains(request.method())) { return outbound.then(); } + // Send with publisher (defer buffer cleanup to netty) return outbound.sendObject(Mono.just(clientCodec.encode(message))).then(); } @@ -139,18 +134,18 @@ public Flux requestChannel( } private static ServiceMessage toMessage(HttpClientResponse httpResponse, ByteBuf data) { - ServiceMessage.Builder builder = - ServiceMessage.builder().qualifier(httpResponse.uri()).data(data); + final var builder = + ServiceMessage.builder() + .qualifier(httpResponse.uri()) + .data(data != Unpooled.EMPTY_BUFFER ? data : null); - HttpResponseStatus status = httpResponse.status(); - if (isError(status)) { - builder.header(ServiceMessage.HEADER_ERROR_TYPE, status.code()); + if (isError(httpResponse.status())) { + builder.header(ServiceMessage.HEADER_ERROR_TYPE, httpResponse.status().code()); } - // prepare response headers + // Populate HTTP response headers httpResponse .responseHeaders() - .entries() .forEach(entry -> builder.header(entry.getKey(), entry.getValue())); return builder.build(); @@ -179,15 +174,16 @@ private static String applyQueryParams(String uri, Map queryPara return uri; } - private static Map extractPrefix(Map headers, String prefix) { + private static Map headersByPrefix(Map headers, String prefix) { if (headers == null || headers.isEmpty()) { return Map.of(); } + final var finalPrefix = prefix + "."; final var result = new HashMap(); headers.forEach( (k, v) -> { - if (k.startsWith(prefix)) { - result.put(k.substring(prefix.length()), v); + if (k.startsWith(finalPrefix)) { + result.put(k.substring(finalPrefix.length()), v); } }); return result; diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java index d243cc8b9..1d762d39d 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java @@ -255,7 +255,8 @@ private static Mono error(HttpServerResponse httpResponse, ServiceMessage ? encodeData(response.data(), response.dataFormatOrDefault()) : ((ByteBuf) response.data()); - // send with publisher (defer buffer cleanup to netty) + // Send with publisher (defer buffer cleanup to netty) + return httpResponse.status(status).send(Mono.just(content)).then(); } @@ -269,7 +270,8 @@ private static Mono ok(HttpServerResponse httpResponse, ServiceMessage res ? ((ByteBuf) response.data()) : encodeData(response.data(), response.dataFormatOrDefault()); - // send with publisher (defer buffer cleanup to netty) + // Send with publisher (defer buffer cleanup to netty) + return httpResponse.status(OK).send(Mono.just(content)).then(); } From 6f967dea2f34087d8e8500f5784680a7acae0c34 Mon Sep 17 00:00:00 2001 From: artem-v Date: Mon, 17 Nov 2025 23:20:04 +0200 Subject: [PATCH 04/17] WIP --- .../client/http/HttpGatewayClientTransport.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java index 1ffea2eb8..11b88d2d9 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java @@ -111,8 +111,17 @@ public Mono requestResponse(ServiceMessage message, Type respons private Mono send( ServiceMessage message, HttpClientRequest request, NettyOutbound outbound) { - // Populate HTTP request headers - message.headers().forEach(request::header); + // Extract custom headers + final var messageHeaders = message.headers(); + final var httpHeaders = headersByPrefix(messageHeaders, "http.header"); + + // Apply HTTP headers first + httpHeaders.forEach(request::header); + + // Apply remaining message headers (skip http.*) + messageHeaders.entrySet().stream() + .filter(e -> !e.getKey().startsWith("http.")) + .forEach(e -> request.header(e.getKey(), e.getValue())); if (BODYLESS_METHODS.contains(request.method())) { return outbound.then(); From d83647430d44ec7c9d6ad8667e2e57e3525fbd2c Mon Sep 17 00:00:00 2001 From: artem-v Date: Mon, 17 Nov 2025 23:34:12 +0200 Subject: [PATCH 05/17] WIp --- .../services/gateway/http/HttpGatewayAcceptor.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java index 1d762d39d..214b388d8 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java @@ -214,13 +214,17 @@ private static ServiceMessage toMessage( HttpServerRequest httpRequest, Consumer consumer) { final var builder = ServiceMessage.builder(); - // Copy http headers to service message + // Copy HTTP method + + builder.header("http.method", httpRequest.method().name()); + + // Copy HTTP headers to service message for (var httpHeader : httpRequest.requestHeaders()) { - builder.header(httpHeader.getKey(), httpHeader.getValue()); + builder.header("http.header." + httpHeader.getKey(), httpHeader.getValue()); } - // Add http method to service message (used by REST services) + // Add HTTP method to service message (used by REST services) builder .header(HEADER_REQUEST_METHOD, httpRequest.method().name()) From e83627283b40de3d9704e2770a6da5825cac020e Mon Sep 17 00:00:00 2001 From: artem-v Date: Tue, 18 Nov 2025 00:21:37 +0200 Subject: [PATCH 06/17] WIp --- .../scalecube/services/gateway/http/HttpGatewayAcceptor.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java index 214b388d8..8b448ffba 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java @@ -214,10 +214,6 @@ private static ServiceMessage toMessage( HttpServerRequest httpRequest, Consumer consumer) { final var builder = ServiceMessage.builder(); - // Copy HTTP method - - builder.header("http.method", httpRequest.method().name()); - // Copy HTTP headers to service message for (var httpHeader : httpRequest.requestHeaders()) { @@ -227,6 +223,7 @@ private static ServiceMessage toMessage( // Add HTTP method to service message (used by REST services) builder + .header("http.method", httpRequest.method().name()) .header(HEADER_REQUEST_METHOD, httpRequest.method().name()) .qualifier(httpRequest.uri().substring(1)); if (consumer != null) { From c0062659433d2c796ccaa8c0269d6b5f163ec63d Mon Sep 17 00:00:00 2001 From: artem-v Date: Tue, 18 Nov 2025 00:58:14 +0200 Subject: [PATCH 07/17] WIP --- .../io/scalecube/services/ServiceCall.java | 8 +++-- .../http/HttpGatewayClientTransport.java | 5 +-- .../gateway/http/HttpGatewayAcceptor.java | 24 +++++++++++++ .../gateway/rest/RestGatewayTest.java | 25 ++++++++++---- .../gateway/rest/RestServiceImpl.java | 34 +++++++++++++------ 5 files changed, 75 insertions(+), 21 deletions(-) diff --git a/services-api/src/main/java/io/scalecube/services/ServiceCall.java b/services-api/src/main/java/io/scalecube/services/ServiceCall.java index fa8d396a8..5e7cce338 100644 --- a/services-api/src/main/java/io/scalecube/services/ServiceCall.java +++ b/services-api/src/main/java/io/scalecube/services/ServiceCall.java @@ -425,13 +425,17 @@ private Optional toStringOrEqualsOrHashCode( private static Function, Flux> asFlux( boolean isReturnTypeServiceMessage) { return flux -> - isReturnTypeServiceMessage ? flux.cast(Object.class) : flux.map(ServiceMessage::data); + isReturnTypeServiceMessage + ? flux.cast(Object.class) + : flux.mapNotNull(ServiceMessage::data); } private static Function, Mono> asMono( boolean isReturnTypeServiceMessage) { return mono -> - isReturnTypeServiceMessage ? mono.cast(Object.class) : mono.map(ServiceMessage::data); + isReturnTypeServiceMessage + ? mono.cast(Object.class) + : mono.mapNotNull(ServiceMessage::data); } private ServiceMessage throwIfError(ServiceMessage message) { diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java index 11b88d2d9..6db709dc3 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java @@ -148,8 +148,9 @@ private static ServiceMessage toMessage(HttpClientResponse httpResponse, ByteBuf .qualifier(httpResponse.uri()) .data(data != Unpooled.EMPTY_BUFFER ? data : null); - if (isError(httpResponse.status())) { - builder.header(ServiceMessage.HEADER_ERROR_TYPE, httpResponse.status().code()); + final var status = httpResponse.status(); + if (isError(status)) { + builder.header(ServiceMessage.HEADER_ERROR_TYPE, status.code()); } // Populate HTTP response headers diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java index 8b448ffba..f146cfaa1 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java @@ -30,11 +30,15 @@ import io.scalecube.services.routing.StaticAddressRouter; import io.scalecube.services.transport.api.DataCodec; import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -220,12 +224,18 @@ private static ServiceMessage toMessage( builder.header("http.header." + httpHeader.getKey(), httpHeader.getValue()); } + // Copy HTTP query params to service message + + final var queryParams = matchQueryParams(httpRequest.uri()); + queryParams.forEach((param, value) -> builder.header("http.query." + param, value)); + // Add HTTP method to service message (used by REST services) builder .header("http.method", httpRequest.method().name()) .header(HEADER_REQUEST_METHOD, httpRequest.method().name()) .qualifier(httpRequest.uri().substring(1)); + if (consumer != null) { consumer.accept(builder); } @@ -233,6 +243,20 @@ private static ServiceMessage toMessage( return builder.build(); } + private static Map matchQueryParams(String uri) { + final var index = uri.indexOf('?'); + if (index < 0 || index == uri.length() - 1) { + return Collections.emptyMap(); // no query params + } + return Arrays.stream(uri.substring(index + 1).split("&")) + .map(s -> s.split("=", 2)) + .filter(parts -> parts.length == 2) + .collect( + Collectors.toMap( + parts -> URLDecoder.decode(parts[0], StandardCharsets.UTF_8), + parts -> URLDecoder.decode(parts[1], StandardCharsets.UTF_8))); + } + private static Mono emptyMessage(ServiceMessage message) { return Mono.just(ServiceMessage.builder().qualifier(message.qualifier()).build()); } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java index 4b85cf9bc..f01733228 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java @@ -1,9 +1,12 @@ package io.scalecube.services.gateway.rest; +import static io.scalecube.services.api.ServiceMessage.HEADER_ERROR_TYPE; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.collection.IsMapContaining.hasKey; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNull; import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonInclude.Include; @@ -43,7 +46,6 @@ public class RestGatewayTest { private static Microservices gateway; private static Microservices microservices; - private static final ObjectMapper objectMapper = objectMapper(); private static Address gatewayAddress; private static StaticAddressRouter router; @@ -167,18 +169,26 @@ void testGet() { @Test void testHead() { final var param = "head" + System.currentTimeMillis(); + final var customHeader1 = "customHeader-" + System.currentTimeMillis(); + final var customHeader2 = "customHeader-" + System.currentTimeMillis(); + final var queryParam1 = "queryParam-" + System.currentTimeMillis(); + final var queryParam2 = "queryParam-" + System.currentTimeMillis(); StepVerifier.create( serviceCall.requestOne( ServiceMessage.builder() .header("http.method", "HEAD") + .header("http.header.X-Custom-Header-1", customHeader1) + .header("http.header.X-Custom-Header-2", customHeader2) + .header("http.query.param1", queryParam1) + .header("http.query.param2", queryParam2) .qualifier("v1/restService/head/" + param) .build(), SomeResponse.class)) .assertNext( message -> { - final var someResponse = message.data(); - assertNotNull(someResponse, "data"); - assertEquals(param, someResponse.name(), "someResponse.name"); + assertNull(message.data(), "data"); + assertNotNull(message.headers(), "headers"); + assertThat(message.headers(), not(hasKey(HEADER_ERROR_TYPE))); }) .verifyComplete(); } @@ -384,9 +394,10 @@ void testNoMatchWithoutRestMethod() { .expectErrorSatisfies( ex -> { final var exception = (ServiceUnavailableException) ex; - final var errorMessage = exception.getMessage(); assertEquals(503, exception.errorCode()); - assertTrue(errorMessage.startsWith("No reachable member with such service")); + assertThat( + exception.getMessage(), + Matchers.startsWith("No reachable member with such service")); }) .verify(Duration.ofSeconds(3)); } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java index 4b44b2285..880f5784d 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java @@ -1,8 +1,11 @@ package io.scalecube.services.gateway.rest; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasKey; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import io.scalecube.services.RequestContext; import reactor.core.publisher.Mono; @@ -17,7 +20,7 @@ public Mono options() { final var foo = context.pathVar("foo"); assertNotNull(foo); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("OPTIONS", context.requestMethod()); return new SomeResponse().name(foo); }); @@ -31,7 +34,7 @@ public Mono get() { final var foo = context.pathVar("foo"); assertNotNull(foo); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("GET", context.requestMethod()); return new SomeResponse().name(foo); }); @@ -44,9 +47,20 @@ public Mono head() { context -> { final var foo = context.pathVar("foo"); assertNotNull(foo); - assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + final var headers = context.headers(); + assertNotNull(headers); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("HEAD", context.requestMethod()); + + assertThat( + headers, + allOf( + hasKey("http.method"), + hasKey("http.header.X-Custom-Header-1"), + hasKey("http.header.X-Custom-Header-2"), + hasKey("http.query.param1"), + hasKey("http.query.param2"))); + return new SomeResponse().name(foo); }); } @@ -58,7 +72,7 @@ public Mono post(SomeRequest request) { context -> { assertNotNull(context.pathVar("foo")); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("POST", context.requestMethod()); return new SomeResponse().name(request.name()); }); @@ -71,7 +85,7 @@ public Mono put(SomeRequest request) { context -> { assertNotNull(context.pathVar("foo")); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("PUT", context.requestMethod()); return new SomeResponse().name(request.name()); }); @@ -84,7 +98,7 @@ public Mono patch(SomeRequest request) { context -> { assertNotNull(context.pathVar("foo")); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("PATCH", context.requestMethod()); return new SomeResponse().name(request.name()); }); @@ -98,7 +112,7 @@ public Mono delete() { final var foo = context.pathVar("foo"); assertNotNull(foo); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("DELETE", context.requestMethod()); return new SomeResponse().name(foo); }); @@ -112,7 +126,7 @@ public Mono trace() { final var foo = context.pathVar("foo"); assertNotNull(foo); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("TRACE", context.requestMethod()); return new SomeResponse().name(foo); }); From 916339dfe9d4cf8354bf7e8bf1a336bac5c3586f Mon Sep 17 00:00:00 2001 From: artem-v Date: Tue, 18 Nov 2025 09:12:25 +0200 Subject: [PATCH 08/17] WIP --- .../services/api/DynamicQualifier.java | 6 ++++-- .../services/api/DynamicQualifierTest.java | 21 +++++++++++++++++++ .../gateway/rest/RestGatewayTest.java | 2 +- .../gateway/rest/RestServiceImpl.java | 2 +- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java b/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java index 36c5417de..3b40bb507 100644 --- a/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java +++ b/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java @@ -113,11 +113,13 @@ public int size() { * @return matched path variables key-value map, or null if no matching occurred */ public Map matchQualifier(String qualifier) { - if (size != sizeOf(qualifier)) { + final var path = qualifier.split("\\?")[0]; + + if (size != sizeOf(path)) { return null; } - final var matcher = pattern.matcher(qualifier); + final var matcher = pattern.matcher(path); if (!matcher.matches()) { return null; } diff --git a/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java b/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java index 6a8bee1c3..58ce5a560 100644 --- a/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java +++ b/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java @@ -101,4 +101,25 @@ void testMatchMultiplePathVariables() { assertEquals("456", map.get("bar")); assertEquals("678", map.get("baz")); } + + @Test + void testMatchWithQueryString() { + final var qualifier = DynamicQualifier.from("v1/this.is.namespace/foo/:foo/bar/:bar"); + final var map = qualifier.matchQualifier("v1/this.is.namespace/foo/123/bar/456?x=1&y=2"); + + assertNotNull(map); + assertEquals("123", map.get("foo")); + assertEquals("456", map.get("bar")); + } + + @Test + void testMatchMultipleVariablesWithQueryStringIgnored() { + final var qualifier = DynamicQualifier.from("v1/this.is.namespace/foo/:foo/bar/:bar"); + + final var map = qualifier.matchQualifier("v1/this.is.namespace/foo/123/bar/456?debug=true"); + + assertNotNull(map); + assertEquals("123", map.get("foo")); + assertEquals("456", map.get("bar")); + } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java index f01733228..8232881e0 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java @@ -168,7 +168,7 @@ void testGet() { @Test void testHead() { - final var param = "head" + System.currentTimeMillis(); + final var param = "head123456"; final var customHeader1 = "customHeader-" + System.currentTimeMillis(); final var customHeader2 = "customHeader-" + System.currentTimeMillis(); final var queryParam1 = "queryParam-" + System.currentTimeMillis(); diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java index 880f5784d..ee4a39d8f 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java @@ -46,7 +46,7 @@ public Mono head() { .map( context -> { final var foo = context.pathVar("foo"); - assertNotNull(foo); + assertEquals("head123456", foo, "pathParam"); final var headers = context.headers(); assertNotNull(headers); assertThat(context.headers().size(), greaterThan(0)); From 7e60062a08bbd8eedc0fa589cfdb2c8dd7e677f8 Mon Sep 17 00:00:00 2001 From: artem-v Date: Tue, 18 Nov 2025 09:19:52 +0200 Subject: [PATCH 09/17] WIP --- .../services/api/DynamicQualifier.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java b/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java index 3b40bb507..dacac78f1 100644 --- a/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java +++ b/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java @@ -25,7 +25,7 @@ public final class DynamicQualifier { private final String qualifier; private final Pattern pattern; - private final List pathVariables; + private final List pathParams; private final int size; private DynamicQualifier(String qualifier) { @@ -34,9 +34,9 @@ private DynamicQualifier(String qualifier) { for (var s : qualifier.split("/")) { if (s.startsWith(":")) { - final var pathVar = s.substring(1); - builder.append("(?<").append(pathVar).append(">.+)"); - list.add(pathVar); + final var param = s.substring(1); + builder.append("(?<").append(param).append(">.+)"); + list.add(param); } else { builder.append(s); } @@ -46,7 +46,7 @@ private DynamicQualifier(String qualifier) { this.qualifier = qualifier; this.pattern = Pattern.compile(builder.toString()); - this.pathVariables = Collections.unmodifiableList(list); + this.pathParams = Collections.unmodifiableList(list); this.size = sizeOf(qualifier); } @@ -93,8 +93,8 @@ public Pattern pattern() { * * @return path variable names */ - public List pathVariables() { - return pathVariables; + public List pathParams() { + return pathParams; } /** @@ -125,12 +125,12 @@ public Map matchQualifier(String qualifier) { } final var map = new LinkedHashMap(); - for (var pathVar : pathVariables) { - final var value = matcher.group(pathVar); + for (var param : pathParams) { + final var value = matcher.group(param); if (value == null || value.isEmpty()) { - throw new IllegalArgumentException("Wrong path variable: " + pathVar); + throw new IllegalArgumentException("Wrong path param: " + param); } - map.put(pathVar, value); + map.put(param, value); } return map; @@ -167,7 +167,7 @@ public String toString() { return new StringJoiner(", ", DynamicQualifier.class.getSimpleName() + "[", "]") .add("qualifier='" + qualifier + "'") .add("pattern=" + pattern) - .add("pathVariables=" + pathVariables) + .add("pathParams=" + pathParams) .add("size=" + size) .toString(); } From a34f80384ee0b96167deb856780ccdcbfe34fda7 Mon Sep 17 00:00:00 2001 From: artem-v Date: Tue, 18 Nov 2025 09:29:30 +0200 Subject: [PATCH 10/17] WIP --- .../java/io/scalecube/services/api/DynamicQualifier.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java b/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java index dacac78f1..958052875 100644 --- a/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java +++ b/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java @@ -98,7 +98,7 @@ public List pathParams() { } /** - * Size of qualifier. This is a number of {@code /} symbols. + * Size of qualifier. A number of {@code /} symbols. * * @return result */ @@ -136,10 +136,10 @@ public Map matchQualifier(String qualifier) { return map; } - private static int sizeOf(String value) { + private static int sizeOf(String path) { int count = 0; - for (int i = 0, length = value.length(); i < length; i++) { - if (value.charAt(i) == '/') { + for (int i = 0, length = path.length(); i < length; i++) { + if (path.charAt(i) == '/') { count++; } } From 16a76ecc9d89158bcc49bf11a5ce68cb54143d00 Mon Sep 17 00:00:00 2001 From: artem-v Date: Tue, 18 Nov 2025 13:54:51 +0200 Subject: [PATCH 11/17] Renamings --- .../io/scalecube/services/api/DynamicQualifier.java | 10 +++++----- .../scalecube/services/api/DynamicQualifierTest.java | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java b/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java index 958052875..fec655c69 100644 --- a/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java +++ b/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java @@ -89,9 +89,9 @@ public Pattern pattern() { } /** - * Returns path variable names. + * Returns path parameter names. * - * @return path variable names + * @return path parameter names */ public List pathParams() { return pathParams; @@ -107,10 +107,10 @@ public int size() { } /** - * Matches input qualifier against this dynamic qualifier. + * Matches given qualifier string with this {@link DynamicQualifier}. * - * @param qualifier qualifier - * @return matched path variables key-value map, or null if no matching occurred + * @param qualifier qualifier string + * @return matched path parameters key-value map, or null if no matching occurred */ public Map matchQualifier(String qualifier) { final var path = qualifier.split("\\?")[0]; diff --git a/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java b/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java index 58ce5a560..5b9eb1bba 100644 --- a/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java +++ b/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java @@ -82,7 +82,7 @@ void testEquality() { } @Test - void testMatchSinglePathVariable() { + void testMatchSinglePathParam() { final var userName = UUID.randomUUID().toString(); final var qualifier = DynamicQualifier.from("v1/this.is.namespace/foo/bar/:userName"); final var map = qualifier.matchQualifier("v1/this.is.namespace/foo/bar/" + userName); @@ -92,7 +92,7 @@ void testMatchSinglePathVariable() { } @Test - void testMatchMultiplePathVariables() { + void testMatchMultiplePathParams() { final var qualifier = DynamicQualifier.from("v1/this.is.namespace/foo/:foo/bar/:bar/baz/:baz"); final var map = qualifier.matchQualifier("v1/this.is.namespace/foo/123/bar/456/baz/678"); assertNotNull(map); @@ -113,7 +113,7 @@ void testMatchWithQueryString() { } @Test - void testMatchMultipleVariablesWithQueryStringIgnored() { + void testMatchMultipleWithQueryStringIgnored() { final var qualifier = DynamicQualifier.from("v1/this.is.namespace/foo/:foo/bar/:bar"); final var map = qualifier.matchQualifier("v1/this.is.namespace/foo/123/bar/456?debug=true"); From 686a7d05e34858857f527a5e36aab8110028bcfd Mon Sep 17 00:00:00 2001 From: artem-v Date: Tue, 18 Nov 2025 14:15:23 +0200 Subject: [PATCH 12/17] Renamings --- .../io/scalecube/services/RequestContext.java | 44 +++++++++---------- .../methods/ServiceMethodInvoker.java | 6 +-- .../services/methods/StubServiceImpl.java | 6 +-- .../examples/GreetingServiceImpl.java | 2 +- .../gateway/files/ReportServiceImpl.java | 2 +- .../gateway/rest/RestServiceImpl.java | 16 +++---- .../gateway/rest/RoutingServiceImpl.java | 4 +- .../services/files/FileServiceImpl.java | 2 +- .../services/PlaceholderQualifierTest.java | 8 ++-- .../registry/ServiceRegistryImplTest.java | 20 ++++----- .../services/sut/GreetingServiceImpl.java | 2 +- 11 files changed, 56 insertions(+), 56 deletions(-) diff --git a/services-api/src/main/java/io/scalecube/services/RequestContext.java b/services-api/src/main/java/io/scalecube/services/RequestContext.java index b82bea1aa..67d787130 100644 --- a/services-api/src/main/java/io/scalecube/services/RequestContext.java +++ b/services-api/src/main/java/io/scalecube/services/RequestContext.java @@ -34,7 +34,7 @@ public class RequestContext implements Context { private static final Object REQUEST_KEY = new Object(); private static final Object PRINCIPAL_KEY = new Object(); private static final Object METHOD_INFO_KEY = new Object(); - private static final Object PATH_VARS_KEY = new Object(); + private static final Object PATH_PARAMS_KEY = new Object(); private final Context source; @@ -224,43 +224,43 @@ public RequestContext methodInfo(MethodInfo methodInfo) { } /** - * Returns path variables associated with the request. + * Returns path parameters associated with the request. * - * @return path variables, or {@code null} if not set + * @return path parameters, or {@code null} if not set */ - public Map pathVars() { - return source.getOrDefault(PATH_VARS_KEY, Collections.emptyMap()); + public Map pathParams() { + return source.getOrDefault(PATH_PARAMS_KEY, Collections.emptyMap()); } /** - * Puts path variables associated with the request. + * Puts path parameters associated with the request. * - * @return path variables, or {@code null} if not set + * @return path parameters, or {@code null} if not set */ - public RequestContext pathVars(Map pathVars) { - return put(PATH_VARS_KEY, pathVars); + public RequestContext pathParams(Map pathParams) { + return put(PATH_PARAMS_KEY, pathParams); } /** - * Returns specific path variable by name. + * Returns specific path parameter by name. * - * @param name name of the path variable - * @return path variable value, or {@code null} if not found + * @param name name of the path parameter + * @return path parameter value, or {@code null} if not found */ - public String pathVar(String name) { - return pathVars().get(name); + public String pathParam(String name) { + return pathParams().get(name); } /** - * Returns specific path variable by name, and converts it to the specified type. + * Returns specific path parameter by name, and converts it to the specified type. * - * @param name name of the path variable - * @param type expected type of the variable + * @param name name of the path parameter + * @param type expected type of the path parameter * @param type parameter - * @return converted path variable, or {@code null} if not found + * @return converted path parameter, or {@code null} if not found */ - public T pathVar(String name, Class type) { - final var s = pathVar(name); + public T pathParam(String name, Class type) { + final var s = pathParam(name); if (s == null) { return null; } @@ -286,7 +286,7 @@ public T pathVar(String name, Class type) { return (T) new BigInteger(s); } - throw new IllegalArgumentException("Unsupported pathVar type: " + type); + throw new IllegalArgumentException("Unsupported pathParam type: " + type); } /** @@ -366,7 +366,7 @@ public String toString() { .add("principal=" + principal()) .add("methodInfo=" + methodInfo()) .add("headers=" + mask(headers())) - .add("pathVars=" + mask(pathVars())) + .add("pathParams=" + mask(pathParams())) .add("sourceKeys=" + source.stream().map(Entry::getKey).toList()) .toString(); } diff --git a/services-api/src/main/java/io/scalecube/services/methods/ServiceMethodInvoker.java b/services-api/src/main/java/io/scalecube/services/methods/ServiceMethodInvoker.java index d9bbcdc14..1f56e2abf 100644 --- a/services-api/src/main/java/io/scalecube/services/methods/ServiceMethodInvoker.java +++ b/services-api/src/main/java/io/scalecube/services/methods/ServiceMethodInvoker.java @@ -255,15 +255,15 @@ private Context enhanceRequestContext( RequestContext context, Object request, Principal principal) { final var dynamicQualifier = methodInfo.dynamicQualifier(); - Map pathVars = null; + Map pathParams = null; if (dynamicQualifier != null) { - pathVars = dynamicQualifier.matchQualifier(context.requestQualifier()); + pathParams = dynamicQualifier.matchQualifier(context.requestQualifier()); } return new RequestContext(context) .request(request) .principal(principal) - .pathVars(pathVars) + .pathParams(pathParams) .methodInfo(methodInfo); } diff --git a/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java b/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java index 658911f08..4bac38db5 100644 --- a/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java +++ b/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java @@ -51,9 +51,9 @@ public Mono invokeDynamicQualifier() { context -> { assertNotNull(context.headers(), "headers"); assertNotNull(context.principal(), "principal"); - assertNotNull(context.pathVars(), "pathVars"); - assertNotNull(context.pathVar("foo"), "pathVar[foo]"); - assertNotNull(context.pathVar("bar"), "pathVar[bar]"); + assertNotNull(context.pathParams(), "pathParams"); + assertNotNull(context.pathParam("foo"), "pathParam[foo]"); + assertNotNull(context.pathParam("bar"), "pathParam[bar]"); }) .then(); } diff --git a/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java b/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java index a78f0fc4d..bf230991b 100644 --- a/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java +++ b/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java @@ -107,6 +107,6 @@ public Mono emptyGreetingMessage(ServiceMessage request) { @Override public Mono helloDynamicQualifier(Long value) { return RequestContext.deferContextual() - .map(context -> context.pathVar("someVar") + "@" + value); + .map(context -> context.pathParam("someVar") + "@" + value); } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java index 92ae3fba0..54ee4f985 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java @@ -122,7 +122,7 @@ public Flux successfulDownload() { return RequestContext.deferContextual() .flatMapMany( context -> { - final var fileSize = context.pathVar("fileSize", Long.class); + final var fileSize = context.pathParam("fileSize", Long.class); final var headers = context.headers(); final File file; try { diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java index ee4a39d8f..b9db493e9 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java @@ -17,7 +17,7 @@ public Mono options() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathVar("foo"); + final var foo = context.pathParam("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -31,7 +31,7 @@ public Mono get() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathVar("foo"); + final var foo = context.pathParam("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -45,7 +45,7 @@ public Mono head() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathVar("foo"); + final var foo = context.pathParam("foo"); assertEquals("head123456", foo, "pathParam"); final var headers = context.headers(); assertNotNull(headers); @@ -70,7 +70,7 @@ public Mono post(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathVar("foo")); + assertNotNull(context.pathParam("foo")); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); assertEquals("POST", context.requestMethod()); @@ -83,7 +83,7 @@ public Mono put(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathVar("foo")); + assertNotNull(context.pathParam("foo")); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); assertEquals("PUT", context.requestMethod()); @@ -96,7 +96,7 @@ public Mono patch(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathVar("foo")); + assertNotNull(context.pathParam("foo")); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); assertEquals("PATCH", context.requestMethod()); @@ -109,7 +109,7 @@ public Mono delete() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathVar("foo"); + final var foo = context.pathParam("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -123,7 +123,7 @@ public Mono trace() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathVar("foo"); + final var foo = context.pathParam("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java index ea20ef61f..637a26ea6 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java @@ -14,7 +14,7 @@ public Mono find() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathVar("foo"); + final var foo = context.pathParam("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertTrue(context.headers().size() > 0); @@ -28,7 +28,7 @@ public Mono update(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathVar("foo")); + assertNotNull(context.pathParam("foo")); assertNotNull(context.headers()); assertTrue(context.headers().size() > 0); assertEquals("POST", context.requestMethod()); diff --git a/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java b/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java index e769bfe5f..44e86887d 100644 --- a/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java +++ b/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java @@ -96,7 +96,7 @@ public Flux streamFile() { .flatMapMany( context -> { final var headers = context.headers(); - final var filename = context.pathVar("filename"); + final var filename = context.pathParam("filename"); final var path = baseDir.resolve(filename); if (!isPathValid(path)) { diff --git a/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java b/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java index 89aaf3127..1c70c5d9f 100644 --- a/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java +++ b/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java @@ -86,7 +86,7 @@ void shouldRouteByPlaceholderQualifier() { } @Test - void shouldRouteByPlaceholderQualifierWithPathVar() { + void shouldRouteByPlaceholderQualifierWithPathParam() { final var foo1Id = providerFoo1.id(); final var name1 = "name1"; final String foo1Result = @@ -150,7 +150,7 @@ public interface FooService { Mono hello(); @ServiceMethod("hello/${microservices:id}/:name") - Mono helloWithPathVar(); + Mono helloWithPathParam(); } public static class FooServiceImpl implements FooService { @@ -168,8 +168,8 @@ public Mono hello() { } @Override - public Mono helloWithPathVar() { - return RequestContext.deferContextual().map(context -> id + "|" + context.pathVar("name")); + public Mono helloWithPathParam() { + return RequestContext.deferContextual().map(context -> id + "|" + context.pathParam("name")); } } } diff --git a/services/src/test/java/io/scalecube/services/registry/ServiceRegistryImplTest.java b/services/src/test/java/io/scalecube/services/registry/ServiceRegistryImplTest.java index d4200d701..2e1c466a9 100644 --- a/services/src/test/java/io/scalecube/services/registry/ServiceRegistryImplTest.java +++ b/services/src/test/java/io/scalecube/services/registry/ServiceRegistryImplTest.java @@ -78,7 +78,7 @@ void testRegisterThenUnregisterServiceEndpoint() { new HashMap<>(), List.of( ServiceMethodDefinition.fromAction("hello"), - ServiceMethodDefinition.fromAction("hello/:pathVar"))))) + ServiceMethodDefinition.fromAction("hello/:pathParam"))))) .build()); } @@ -136,7 +136,7 @@ void testLookupService() { new HashMap<>(), List.of( ServiceMethodDefinition.fromAction("hello"), - ServiceMethodDefinition.fromAction("hello/:pathVar"))))) + ServiceMethodDefinition.fromAction("hello/:pathParam"))))) .build()); } assertEquals( @@ -194,8 +194,8 @@ interface HelloTwo { String NAMESPACE = "greeting"; - @ServiceMethod("hello/:pathVar") - default Mono helloPathVar() { + @ServiceMethod("hello/:pathParam") + default Mono helloPathParam() { return Mono.just("" + System.currentTimeMillis()); } } @@ -210,14 +210,14 @@ interface RestServiceOne { String NAMESPACE = "v1/api"; @RestMethod("POST") - @ServiceMethod("foo/:pathVar") - default Mono updateWithPathVar() { + @ServiceMethod("foo/:pathParam") + default Mono updateWithPathParam() { return Mono.just("" + System.currentTimeMillis()); } @RestMethod("POST") @ServiceMethod("foo/update") - default Mono updateWithoutPathVar() { + default Mono updateWithoutPathParam() { return Mono.just("" + System.currentTimeMillis()); } } @@ -228,14 +228,14 @@ interface RestServiceTwo { String NAMESPACE = "v1/api"; @RestMethod("PUT") - @ServiceMethod("foo/:pathVar") - default Mono updateWithPathVar() { + @ServiceMethod("foo/:pathParam") + default Mono updateWithPathParam() { return Mono.just("" + System.currentTimeMillis()); } @RestMethod("PUT") @ServiceMethod("foo/update") - default Mono updateWithoutPathVar() { + default Mono updateWithoutPathParam() { return Mono.just("" + System.currentTimeMillis()); } } diff --git a/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java b/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java index fd6dc6ecb..d0a470f9e 100644 --- a/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java +++ b/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java @@ -205,6 +205,6 @@ public Flux manyStream(Long cnt) { @Override public Mono helloDynamicQualifier(Long value) { return RequestContext.deferContextual() - .map(context -> context.pathVar("someVar") + "@" + value); + .map(context -> context.pathParam("someVar") + "@" + value); } } From addf07b7ead585d2be5579e296075890cc3f3163 Mon Sep 17 00:00:00 2001 From: artem-v Date: Tue, 18 Nov 2025 16:45:30 +0200 Subject: [PATCH 13/17] Added typed properties, + renamigns --- .../io/scalecube/services/RequestContext.java | 56 +------------------ .../scalecube/services/TypedParameters.java | 53 ++++++++++++++++++ .../services/methods/StubServiceImpl.java | 7 ++- .../examples/GreetingServiceImpl.java | 2 +- .../gateway/files/ReportServiceImpl.java | 3 +- .../gateway/rest/RestServiceImpl.java | 27 ++++++--- .../gateway/rest/RoutingServiceImpl.java | 7 ++- .../services/files/FileServiceImpl.java | 3 +- .../services/PlaceholderQualifierTest.java | 3 +- .../services/sut/GreetingServiceImpl.java | 2 +- 10 files changed, 92 insertions(+), 71 deletions(-) create mode 100644 services-api/src/main/java/io/scalecube/services/TypedParameters.java diff --git a/services-api/src/main/java/io/scalecube/services/RequestContext.java b/services-api/src/main/java/io/scalecube/services/RequestContext.java index 67d787130..55caeac29 100644 --- a/services-api/src/main/java/io/scalecube/services/RequestContext.java +++ b/services-api/src/main/java/io/scalecube/services/RequestContext.java @@ -9,8 +9,6 @@ import io.scalecube.services.auth.Principal; import io.scalecube.services.exceptions.ForbiddenException; import io.scalecube.services.methods.MethodInfo; -import java.math.BigDecimal; -import java.math.BigInteger; import java.util.Collections; import java.util.Map; import java.util.Map.Entry; @@ -228,8 +226,8 @@ public RequestContext methodInfo(MethodInfo methodInfo) { * * @return path parameters, or {@code null} if not set */ - public Map pathParams() { - return source.getOrDefault(PATH_PARAMS_KEY, Collections.emptyMap()); + public TypedParameters pathParams() { + return new TypedParameters(source.getOrDefault(PATH_PARAMS_KEY, Collections.emptyMap())); } /** @@ -241,54 +239,6 @@ public RequestContext pathParams(Map pathParams) { return put(PATH_PARAMS_KEY, pathParams); } - /** - * Returns specific path parameter by name. - * - * @param name name of the path parameter - * @return path parameter value, or {@code null} if not found - */ - public String pathParam(String name) { - return pathParams().get(name); - } - - /** - * Returns specific path parameter by name, and converts it to the specified type. - * - * @param name name of the path parameter - * @param type expected type of the path parameter - * @param type parameter - * @return converted path parameter, or {@code null} if not found - */ - public T pathParam(String name, Class type) { - final var s = pathParam(name); - if (s == null) { - return null; - } - - if (type == String.class) { - //noinspection unchecked - return (T) s; - } - if (type == Integer.class) { - //noinspection unchecked - return (T) Integer.valueOf(s); - } - if (type == Long.class) { - //noinspection unchecked - return (T) Long.valueOf(s); - } - if (type == BigDecimal.class) { - //noinspection unchecked - return (T) new BigDecimal(s); - } - if (type == BigInteger.class) { - //noinspection unchecked - return (T) new BigInteger(s); - } - - throw new IllegalArgumentException("Unsupported pathParam type: " + type); - } - /** * Retrieves {@link RequestContext} from the reactor context, wrapping the existing context if * necessary. @@ -366,7 +316,7 @@ public String toString() { .add("principal=" + principal()) .add("methodInfo=" + methodInfo()) .add("headers=" + mask(headers())) - .add("pathParams=" + mask(pathParams())) + .add("pathParams=" + source.getOrDefault(PATH_PARAMS_KEY, Collections.emptyMap())) .add("sourceKeys=" + source.stream().map(Entry::getKey).toList()) .toString(); } diff --git a/services-api/src/main/java/io/scalecube/services/TypedParameters.java b/services-api/src/main/java/io/scalecube/services/TypedParameters.java new file mode 100644 index 000000000..da838625d --- /dev/null +++ b/services-api/src/main/java/io/scalecube/services/TypedParameters.java @@ -0,0 +1,53 @@ +package io.scalecube.services; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +public class TypedParameters { + + private final Map params; + + public TypedParameters(Map params) { + this.params = new LinkedHashMap<>(params != null ? params : Map.of()); + } + + public Integer intValue(String name) { + return get(name, Integer::parseInt); + } + + public Long longValue(String name) { + return get(name, Long::parseLong); + } + + public BigInteger bigInteger(String name) { + return get(name, BigInteger::new); + } + + public Double doubleValue(String name) { + return get(name, Double::parseDouble); + } + + public BigDecimal bigDecimal(String name) { + return get(name, BigDecimal::new); + } + + public > T enumValue(String name, Function enumFunc) { + return get(name, enumFunc); + } + + public Boolean booleanValue(String name) { + return get(name, Boolean::parseBoolean); + } + + public String stringValue(String name) { + return get(name, s -> s); + } + + public T get(String name, Function converter) { + final var s = params.get(name); + return s != null ? converter.apply(s) : null; + } +} diff --git a/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java b/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java index 4bac38db5..29652aab9 100644 --- a/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java +++ b/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java @@ -51,9 +51,10 @@ public Mono invokeDynamicQualifier() { context -> { assertNotNull(context.headers(), "headers"); assertNotNull(context.principal(), "principal"); - assertNotNull(context.pathParams(), "pathParams"); - assertNotNull(context.pathParam("foo"), "pathParam[foo]"); - assertNotNull(context.pathParam("bar"), "pathParam[bar]"); + final var pathParams = context.pathParams(); + assertNotNull(pathParams, "pathParams"); + assertNotNull(pathParams.stringValue("foo"), "pathParam[foo]"); + assertNotNull(pathParams.stringValue("bar"), "pathParam[bar]"); }) .then(); } diff --git a/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java b/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java index bf230991b..1c3bc4b4a 100644 --- a/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java +++ b/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java @@ -107,6 +107,6 @@ public Mono emptyGreetingMessage(ServiceMessage request) { @Override public Mono helloDynamicQualifier(Long value) { return RequestContext.deferContextual() - .map(context -> context.pathParam("someVar") + "@" + value); + .map(context -> context.pathParams().stringValue("someVar") + "@" + value); } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java index 54ee4f985..30c27ce0a 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java @@ -122,7 +122,8 @@ public Flux successfulDownload() { return RequestContext.deferContextual() .flatMapMany( context -> { - final var fileSize = context.pathParam("fileSize", Long.class); + final var pathParams = context.pathParams(); + final var fileSize = pathParams.longValue("fileSize"); final var headers = context.headers(); final File file; try { diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java index b9db493e9..2e2f6e23d 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java @@ -17,7 +17,8 @@ public Mono options() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathParam("foo"); + final var pathParams = context.pathParams(); + final var foo = pathParams.stringValue("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -31,7 +32,8 @@ public Mono get() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathParam("foo"); + final var pathParams = context.pathParams(); + final var foo = pathParams.stringValue("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -45,7 +47,8 @@ public Mono head() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathParam("foo"); + final var pathParams = context.pathParams(); + final var foo = pathParams.stringValue("foo"); assertEquals("head123456", foo, "pathParam"); final var headers = context.headers(); assertNotNull(headers); @@ -70,7 +73,9 @@ public Mono post(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathParam("foo")); + final var pathParams = context.pathParams(); + final var foo = pathParams.stringValue("foo"); + assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); assertEquals("POST", context.requestMethod()); @@ -83,7 +88,9 @@ public Mono put(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathParam("foo")); + final var pathParams = context.pathParams(); + final var foo = pathParams.stringValue("foo"); + assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); assertEquals("PUT", context.requestMethod()); @@ -96,7 +103,9 @@ public Mono patch(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathParam("foo")); + final var pathParams = context.pathParams(); + final var foo = pathParams.stringValue("foo"); + assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); assertEquals("PATCH", context.requestMethod()); @@ -109,7 +118,8 @@ public Mono delete() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathParam("foo"); + final var pathParams = context.pathParams(); + final var foo = pathParams.stringValue("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -123,7 +133,8 @@ public Mono trace() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathParam("foo"); + final var pathParams = context.pathParams(); + final var foo = pathParams.stringValue("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java index 637a26ea6..f36c025f3 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java @@ -14,7 +14,8 @@ public Mono find() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathParam("foo"); + final var pathParams = context.pathParams(); + final var foo = pathParams.stringValue("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertTrue(context.headers().size() > 0); @@ -28,7 +29,9 @@ public Mono update(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathParam("foo")); + final var pathParams = context.pathParams(); + final var foo = pathParams.stringValue("foo"); + assertNotNull(foo); assertNotNull(context.headers()); assertTrue(context.headers().size() > 0); assertEquals("POST", context.requestMethod()); diff --git a/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java b/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java index 44e86887d..cc6a1386f 100644 --- a/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java +++ b/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java @@ -96,7 +96,8 @@ public Flux streamFile() { .flatMapMany( context -> { final var headers = context.headers(); - final var filename = context.pathParam("filename"); + final var pathParams = context.pathParams(); + final var filename = pathParams.stringValue("filename"); final var path = baseDir.resolve(filename); if (!isPathValid(path)) { diff --git a/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java b/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java index 1c70c5d9f..0245bf298 100644 --- a/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java +++ b/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java @@ -169,7 +169,8 @@ public Mono hello() { @Override public Mono helloWithPathParam() { - return RequestContext.deferContextual().map(context -> id + "|" + context.pathParam("name")); + return RequestContext.deferContextual() + .map(context -> id + "|" + context.pathParams().stringValue("name")); } } } diff --git a/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java b/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java index d0a470f9e..a151fa5d6 100644 --- a/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java +++ b/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java @@ -205,6 +205,6 @@ public Flux manyStream(Long cnt) { @Override public Mono helloDynamicQualifier(Long value) { return RequestContext.deferContextual() - .map(context -> context.pathParam("someVar") + "@" + value); + .map(context -> context.pathParams().stringValue("someVar") + "@" + value); } } From 3c5ddaa0a4109af96727d8b3891b8092bd6ad5bf Mon Sep 17 00:00:00 2001 From: artem-v Date: Tue, 18 Nov 2025 17:04:34 +0200 Subject: [PATCH 14/17] WIP --- .../gateway/rest/RestGatewayTest.java | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java index 8232881e0..7a8885ef2 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java @@ -8,13 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import io.scalecube.services.Address; import io.scalecube.services.Microservices; import io.scalecube.services.Microservices.Context; @@ -94,19 +87,6 @@ static void afterAll() { } } - private static ObjectMapper objectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); - mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); - mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - mapper.setVisibility(PropertyAccessor.ALL, Visibility.ANY); - mapper.setSerializationInclusion(Include.NON_NULL); - mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); - mapper.registerModule(new JavaTimeModule()); - return mapper; - } - @Nested @TestInstance(Lifecycle.PER_CLASS) class GatewayTests { @@ -186,7 +166,7 @@ void testHead() { SomeResponse.class)) .assertNext( message -> { - assertNull(message.data(), "data"); + assertNull(message.data(), "data"); // this is HEAD assertNotNull(message.headers(), "headers"); assertThat(message.headers(), not(hasKey(HEADER_ERROR_TYPE))); }) From b2ef67c3498029732027abe8ecc32db8b4c5c503 Mon Sep 17 00:00:00 2001 From: artem-v Date: Tue, 18 Nov 2025 18:47:09 +0200 Subject: [PATCH 15/17] Enhanced RequestContext with headerParams() methods, + fixed javadocs --- .../io/scalecube/services/RequestContext.java | 51 ++++++++++++++++--- .../gateway/rest/RestGatewayTest.java | 23 +++++++++ .../services/gateway/rest/RestService.java | 5 ++ .../gateway/rest/RestServiceImpl.java | 32 ++++++++++++ 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/services-api/src/main/java/io/scalecube/services/RequestContext.java b/services-api/src/main/java/io/scalecube/services/RequestContext.java index 55caeac29..ca32ad761 100644 --- a/services-api/src/main/java/io/scalecube/services/RequestContext.java +++ b/services-api/src/main/java/io/scalecube/services/RequestContext.java @@ -10,6 +10,7 @@ import io.scalecube.services.exceptions.ForbiddenException; import io.scalecube.services.methods.MethodInfo; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -100,17 +101,40 @@ public Context delete(Object key) { /** * Returns request headers. * - * @return headers, or {@code null} if not set + * @return headers, or empty map if not set */ public Map headers() { return source.getOrDefault(HEADERS_KEY, Collections.emptyMap()); } + /** + * Returns typed access to all request headers. + * + * @return typed parameters for headers + */ + public TypedParameters headerParams() { + return new TypedParameters(headers()); + } + + /** + * Returns typed access to request headers filtered by the given prefix. Headers matching + * "{prefix}." are included with the prefix stripped from the key. + * + * @param prefix header prefix to filter by (if null or empty, returns all headers) + * @return typed parameters for filtered headers + */ + public TypedParameters headerParams(String prefix) { + if (prefix == null || prefix.isEmpty()) { + return headerParams(); + } + return new TypedParameters(filterByPrefix(headers(), prefix)); + } + /** * Puts request headers to the context. * * @param headers headers - * @return new {@code RequestContext} instance with updated headers + * @return new {@code RequestContext} */ public RequestContext headers(Map headers) { return put(HEADERS_KEY, headers); @@ -129,7 +153,7 @@ public Object request() { * Puts request to the context. * * @param request request - * @return new {@code RequestContext} instance with updated request + * @return new {@code RequestContext} */ public RequestContext request(Object request) { return put(REQUEST_KEY, request); @@ -185,7 +209,7 @@ public Principal principal() { * Puts principal to the context. * * @param principal principal - * @return new {@code RequestContext} instance with the updated principal + * @return new {@code RequestContext} */ public RequestContext principal(Principal principal) { return put(PRINCIPAL_KEY, principal); @@ -224,7 +248,7 @@ public RequestContext methodInfo(MethodInfo methodInfo) { /** * Returns path parameters associated with the request. * - * @return path parameters, or {@code null} if not set + * @return path parameters, or empty map if not set */ public TypedParameters pathParams() { return new TypedParameters(source.getOrDefault(PATH_PARAMS_KEY, Collections.emptyMap())); @@ -233,7 +257,7 @@ public TypedParameters pathParams() { /** * Puts path parameters associated with the request. * - * @return path parameters, or {@code null} if not set + * @return new {@code RequestContext} */ public RequestContext pathParams(Map pathParams) { return put(PATH_PARAMS_KEY, pathParams); @@ -310,6 +334,21 @@ public static Mono deferSecured() { }); } + private static Map filterByPrefix(Map map, String prefix) { + if (map == null || map.isEmpty()) { + return Map.of(); + } + final var finalPrefix = prefix + "."; + final var result = new HashMap(); + map.forEach( + (k, v) -> { + if (k.startsWith(finalPrefix)) { + result.put(k.substring(finalPrefix.length()), v); + } + }); + return result; + } + @Override public String toString() { return new StringJoiner(", ", RequestContext.class.getSimpleName() + "[", "]") diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java index 7a8885ef2..d7ab4abdd 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java @@ -275,6 +275,29 @@ void testTrace() { }) .verifyComplete(); } + + @Test + void testAttributesPropagation() { + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "GET") + .header("http.header.X-String-Header", "abc") + .header("http.header.X-Int-Header", "123456789") + .header("http.query.debug", "true") + .header("http.query.x", "1") + .header("http.query.y", "2") + .qualifier("v1/restService/propagate/123/bar456/baz789") + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertNotNull(someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); + } } @Nested diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java index 9cc2793e0..56f174320 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java @@ -1,5 +1,6 @@ package io.scalecube.services.gateway.rest; +import io.scalecube.services.annotations.RestMethod; import io.scalecube.services.annotations.Service; import io.scalecube.services.annotations.ServiceMethod; import reactor.core.publisher.Mono; @@ -30,4 +31,8 @@ public interface RestService { @ServiceMethod("trace/:foo") Mono trace(); + + @RestMethod("GET") + @ServiceMethod("propagate/:foo/:bar/:baz") + Mono propagateRequestAttributes(); } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java index 2e2f6e23d..5e024cdab 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import io.scalecube.services.RequestContext; +import java.util.UUID; import reactor.core.publisher.Mono; public class RestServiceImpl implements RestService { @@ -142,4 +143,35 @@ public Mono trace() { return new SomeResponse().name(foo); }); } + + @Override + public Mono propagateRequestAttributes() { + return RequestContext.deferContextual() + .map( + context -> { + final var pathParams = context.pathParams(); + assertEquals(123, pathParams.intValue("foo"), "foo"); + assertEquals("bar456", pathParams.stringValue("bar"), "bar"); + assertEquals("baz789", pathParams.stringValue("baz"), "baz"); + + final var headers = context.headers(); + assertEquals("GET", headers.get("http.method")); + assertEquals("abc", headers.get("http.header.X-String-Header")); + assertEquals("123456789", headers.get("http.header.X-Int-Header")); + assertEquals("true", headers.get("http.query.debug")); + assertEquals("1", headers.get("http.query.x")); + assertEquals("2", headers.get("http.query.y")); + + final var httpHeaders = context.headerParams("http.header"); + assertEquals("abc", httpHeaders.stringValue("X-String-Header")); + assertEquals(123456789, httpHeaders.intValue("X-Int-Header")); + + final var queryParams = context.headerParams("http.query"); + assertEquals(true, queryParams.booleanValue("debug")); + assertEquals(1, queryParams.intValue("x")); + assertEquals(2, queryParams.intValue("y")); + + return new SomeResponse().name(UUID.randomUUID().toString()); + }); + } } From 854617dd2e2ac90f6fee0c7b283333339b91754f Mon Sep 17 00:00:00 2001 From: artem-v Date: Wed, 19 Nov 2025 08:00:13 +0200 Subject: [PATCH 16/17] Renamed methods in TypedParameters --- .../scalecube/services/TypedParameters.java | 16 +++++----- .../services/methods/StubServiceImpl.java | 4 +-- .../examples/GreetingServiceImpl.java | 2 +- .../gateway/files/ReportServiceImpl.java | 2 +- .../gateway/rest/RestServiceImpl.java | 32 +++++++++---------- .../gateway/rest/RoutingServiceImpl.java | 4 +-- .../services/files/FileServiceImpl.java | 2 +- .../services/PlaceholderQualifierTest.java | 2 +- .../services/sut/GreetingServiceImpl.java | 2 +- 9 files changed, 33 insertions(+), 33 deletions(-) diff --git a/services-api/src/main/java/io/scalecube/services/TypedParameters.java b/services-api/src/main/java/io/scalecube/services/TypedParameters.java index da838625d..68bba22e4 100644 --- a/services-api/src/main/java/io/scalecube/services/TypedParameters.java +++ b/services-api/src/main/java/io/scalecube/services/TypedParameters.java @@ -14,35 +14,35 @@ public TypedParameters(Map params) { this.params = new LinkedHashMap<>(params != null ? params : Map.of()); } - public Integer intValue(String name) { + public Integer getInt(String name) { return get(name, Integer::parseInt); } - public Long longValue(String name) { + public Long getLong(String name) { return get(name, Long::parseLong); } - public BigInteger bigInteger(String name) { + public BigInteger getBigInteger(String name) { return get(name, BigInteger::new); } - public Double doubleValue(String name) { + public Double getDouble(String name) { return get(name, Double::parseDouble); } - public BigDecimal bigDecimal(String name) { + public BigDecimal getBigDecimal(String name) { return get(name, BigDecimal::new); } - public > T enumValue(String name, Function enumFunc) { + public > T getEnum(String name, Function enumFunc) { return get(name, enumFunc); } - public Boolean booleanValue(String name) { + public Boolean getBoolean(String name) { return get(name, Boolean::parseBoolean); } - public String stringValue(String name) { + public String getString(String name) { return get(name, s -> s); } diff --git a/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java b/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java index 29652aab9..bc99c9f4c 100644 --- a/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java +++ b/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java @@ -53,8 +53,8 @@ public Mono invokeDynamicQualifier() { assertNotNull(context.principal(), "principal"); final var pathParams = context.pathParams(); assertNotNull(pathParams, "pathParams"); - assertNotNull(pathParams.stringValue("foo"), "pathParam[foo]"); - assertNotNull(pathParams.stringValue("bar"), "pathParam[bar]"); + assertNotNull(pathParams.getString("foo"), "pathParam[foo]"); + assertNotNull(pathParams.getString("bar"), "pathParam[bar]"); }) .then(); } diff --git a/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java b/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java index 1c3bc4b4a..6b89b5f1a 100644 --- a/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java +++ b/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java @@ -107,6 +107,6 @@ public Mono emptyGreetingMessage(ServiceMessage request) { @Override public Mono helloDynamicQualifier(Long value) { return RequestContext.deferContextual() - .map(context -> context.pathParams().stringValue("someVar") + "@" + value); + .map(context -> context.pathParams().getString("someVar") + "@" + value); } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java index 30c27ce0a..932c9c25d 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java @@ -123,7 +123,7 @@ public Flux successfulDownload() { .flatMapMany( context -> { final var pathParams = context.pathParams(); - final var fileSize = pathParams.longValue("fileSize"); + final var fileSize = pathParams.getLong("fileSize"); final var headers = context.headers(); final File file; try { diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java index 5e024cdab..3838d5a73 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java @@ -19,7 +19,7 @@ public Mono options() { .map( context -> { final var pathParams = context.pathParams(); - final var foo = pathParams.stringValue("foo"); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -34,7 +34,7 @@ public Mono get() { .map( context -> { final var pathParams = context.pathParams(); - final var foo = pathParams.stringValue("foo"); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -49,7 +49,7 @@ public Mono head() { .map( context -> { final var pathParams = context.pathParams(); - final var foo = pathParams.stringValue("foo"); + final var foo = pathParams.getString("foo"); assertEquals("head123456", foo, "pathParam"); final var headers = context.headers(); assertNotNull(headers); @@ -75,7 +75,7 @@ public Mono post(SomeRequest request) { .map( context -> { final var pathParams = context.pathParams(); - final var foo = pathParams.stringValue("foo"); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -90,7 +90,7 @@ public Mono put(SomeRequest request) { .map( context -> { final var pathParams = context.pathParams(); - final var foo = pathParams.stringValue("foo"); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -105,7 +105,7 @@ public Mono patch(SomeRequest request) { .map( context -> { final var pathParams = context.pathParams(); - final var foo = pathParams.stringValue("foo"); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -120,7 +120,7 @@ public Mono delete() { .map( context -> { final var pathParams = context.pathParams(); - final var foo = pathParams.stringValue("foo"); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -135,7 +135,7 @@ public Mono trace() { .map( context -> { final var pathParams = context.pathParams(); - final var foo = pathParams.stringValue("foo"); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertThat(context.headers().size(), greaterThan(0)); @@ -150,9 +150,9 @@ public Mono propagateRequestAttributes() { .map( context -> { final var pathParams = context.pathParams(); - assertEquals(123, pathParams.intValue("foo"), "foo"); - assertEquals("bar456", pathParams.stringValue("bar"), "bar"); - assertEquals("baz789", pathParams.stringValue("baz"), "baz"); + assertEquals(123, pathParams.getInt("foo"), "foo"); + assertEquals("bar456", pathParams.getString("bar"), "bar"); + assertEquals("baz789", pathParams.getString("baz"), "baz"); final var headers = context.headers(); assertEquals("GET", headers.get("http.method")); @@ -163,13 +163,13 @@ public Mono propagateRequestAttributes() { assertEquals("2", headers.get("http.query.y")); final var httpHeaders = context.headerParams("http.header"); - assertEquals("abc", httpHeaders.stringValue("X-String-Header")); - assertEquals(123456789, httpHeaders.intValue("X-Int-Header")); + assertEquals("abc", httpHeaders.getString("X-String-Header")); + assertEquals(123456789, httpHeaders.getInt("X-Int-Header")); final var queryParams = context.headerParams("http.query"); - assertEquals(true, queryParams.booleanValue("debug")); - assertEquals(1, queryParams.intValue("x")); - assertEquals(2, queryParams.intValue("y")); + assertEquals(true, queryParams.getBoolean("debug")); + assertEquals(1, queryParams.getInt("x")); + assertEquals(2, queryParams.getInt("y")); return new SomeResponse().name(UUID.randomUUID().toString()); }); diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java index f36c025f3..9c8950dc9 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java @@ -15,7 +15,7 @@ public Mono find() { .map( context -> { final var pathParams = context.pathParams(); - final var foo = pathParams.stringValue("foo"); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertTrue(context.headers().size() > 0); @@ -30,7 +30,7 @@ public Mono update(SomeRequest request) { .map( context -> { final var pathParams = context.pathParams(); - final var foo = pathParams.stringValue("foo"); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertTrue(context.headers().size() > 0); diff --git a/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java b/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java index cc6a1386f..9d6606a10 100644 --- a/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java +++ b/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java @@ -97,7 +97,7 @@ public Flux streamFile() { context -> { final var headers = context.headers(); final var pathParams = context.pathParams(); - final var filename = pathParams.stringValue("filename"); + final var filename = pathParams.getString("filename"); final var path = baseDir.resolve(filename); if (!isPathValid(path)) { diff --git a/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java b/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java index 0245bf298..3ed0c3b12 100644 --- a/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java +++ b/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java @@ -170,7 +170,7 @@ public Mono hello() { @Override public Mono helloWithPathParam() { return RequestContext.deferContextual() - .map(context -> id + "|" + context.pathParams().stringValue("name")); + .map(context -> id + "|" + context.pathParams().getString("name")); } } } diff --git a/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java b/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java index a151fa5d6..1765c5ec4 100644 --- a/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java +++ b/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java @@ -205,6 +205,6 @@ public Flux manyStream(Long cnt) { @Override public Mono helloDynamicQualifier(Long value) { return RequestContext.deferContextual() - .map(context -> context.pathParams().stringValue("someVar") + "@" + value); + .map(context -> context.pathParams().getString("someVar") + "@" + value); } } From 88d08849658e326d81ef427093d13d3305c8f61d Mon Sep 17 00:00:00 2001 From: artem-v Date: Wed, 19 Nov 2025 14:59:33 +0200 Subject: [PATCH 17/17] Fixed javadoc --- .../io/scalecube/services/routing/StaticAddressRouter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services-api/src/main/java/io/scalecube/services/routing/StaticAddressRouter.java b/services-api/src/main/java/io/scalecube/services/routing/StaticAddressRouter.java index 769aed4ed..d2425f6a5 100644 --- a/services-api/src/main/java/io/scalecube/services/routing/StaticAddressRouter.java +++ b/services-api/src/main/java/io/scalecube/services/routing/StaticAddressRouter.java @@ -81,7 +81,7 @@ public Builder address(Address address) { /** * Setter for whether to apply behavior of {@link CredentialsSupplier}, or not. If it is known * upfront that destination service is secured, then set this flag to {@code true}, in such case - * {@link CredentialsSupplier#credentials(String)} will be invoked. + * {@link CredentialsSupplier#credentials(String, String)}} will be invoked. * * @param secured secured flag * @return this @@ -93,7 +93,7 @@ public Builder secured(boolean secured) { /** * Setter for {@code serviceRole} property, will be used in the invocation of {@link - * CredentialsSupplier#credentials(String)}. + * CredentialsSupplier#credentials(String, String)}. * * @param serviceRole serviceRole * @return this @@ -105,7 +105,7 @@ public Builder serviceRole(String serviceRole) { /** * Setter for {@code serviceName} property, will be used in the invocation of {@link - * CredentialsSupplier#credentials(String)}. + * CredentialsSupplier#credentials(String, String)}. * * @param serviceName serviceName * @return this