Repository: heremaps/naksha
Pull request #420: v2 - Support configurable header limit
Author: login: hirenkp2000
Created: 2025-04-07T12:21:21Z | Merged: 2025-04-08T05:51:43Z



Diff:
diff --git a/.ort.yml b/.ort.yml
index 78700789da..343cb3fda7 100644
--- a/.ort.yml
+++ b/.ort.yml
@@ -47,3 +47,8 @@ excludes:
   - pattern: "provided"
     reason: "PROVIDED_DEPENDENCY_OF"
     comment: "Scope with dependencies provided by the JDK or container at runtime. Not included in released artifacts."
+analyzer:
+  package_managers:
+    Gradle:
+      options:
+        javaVersion: 17
diff --git a/docker/cloud-config.json b/docker/cloud-config.json
index bb7db8f3ae..b4bbd6b3d3 100644
--- a/docker/cloud-config.json
+++ b/docker/cloud-config.json
@@ -3,6 +3,7 @@
     "type": "Feature",
     "httpPort": 7080,
     "requestBodyLimit": 25,
+    "requestHeaderLimit": 16,
     "authMode": "JWT",
     "jwtPvtKeyPath":  "SOME_PVT_KEY_PATH",
     "jwtPubKeyPaths": "SOME_PUB_KEY_PATHS",
diff --git a/gradle.properties b/gradle.properties
index 689208a36f..0a2de6c505 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,5 +10,5 @@ mavenPassword=YourPassword
 # When updating the version, please as well consider:
 # - here-naksha-lib-core/src/main/com/here/naksha/lib/core/NakshaVersion (static property: latest)
 # - here-naksha-app-service/src/main/resources/swagger/openapi.yaml (info.version property)
-version=2.2.6
+version=2.2.7
 
diff --git a/here-naksha-app-service/src/main/java/com/here/naksha/app/service/http/NakshaHttpVerticle.java b/here-naksha-app-service/src/main/java/com/here/naksha/app/service/http/NakshaHttpVerticle.java
index e1f3f7a47c..c72acdf371 100644
--- a/here-naksha-app-service/src/main/java/com/here/naksha/app/service/http/NakshaHttpVerticle.java
+++ b/here-naksha-app-service/src/main/java/com/here/naksha/app/service/http/NakshaHttpVerticle.java
@@ -90,15 +90,6 @@ public final class NakshaHttpVerticle extends AbstractNakshaHubVerticle {
 
   private final NakshaHubConfig hubConfig;
 
-  private static final HttpServerOptions SERVER_OPTIONS = new HttpServerOptions()
-      .setCompressionSupported(true)
-      .setDecompressionSupported(true)
-      .setHandle100ContinueAutomatically(true)
-      .setTcpQuickAck(true)
-      .setTcpFastOpen(true)
-      .setMaxInitialLineLength(16 * 1024 * 1024) // mb
-      .setIdleTimeout(300);
-
   /**
    * Creates a new verticle. Each verticle will be bound to a single IO worker.
    *
@@ -205,7 +196,10 @@ public void start(final @NotNull Promise<Void> startPromise) {
         router.route().failureHandler(this::failureHandler);
 
         // add handler to set max allowed request payload size
-        log.info("Setting Http request body limit to {} MB", hubConfig.requestBodyLimit);
+        log.info(
+            "Setting Http request body limit to {} MB and Header limit to {} KB",
+            hubConfig.requestBodyLimit,
+            hubConfig.requestHeaderLimit);
         router.route()
             .order(-1) // we add this before any other handler
             .handler(BodyHandler.create()
@@ -222,6 +216,16 @@ public void start(final @NotNull Promise<Void> startPromise) {
         // round-robin strategy.
         //
         // https://vertx.io/docs/vertx-core/java/#_server_sharing
+        final HttpServerOptions SERVER_OPTIONS = new HttpServerOptions()
+            .setCompressionSupported(true)
+            .setDecompressionSupported(true)
+            .setHandle100ContinueAutomatically(true)
+            .setTcpQuickAck(true)
+            .setTcpFastOpen(true)
+            .setMaxInitialLineLength(16 * 1024 * 1024) // MB to Bytes
+            .setMaxHeaderSize(hubConfig.requestHeaderLimit * 1024) // KB to Bytes
+            .setIdleTimeout(300);
+
         vertx.createHttpServer(SERVER_OPTIONS)
             .requestHandler(router)
             .connectionHandler(loggingConnectionHandler())
diff --git a/here-naksha-app-service/src/main/resources/swagger/openapi.yaml b/here-naksha-app-service/src/main/resources/swagger/openapi.yaml
index 74cf9f0572..01edabd9b4 100644
--- a/here-naksha-app-service/src/main/resources/swagger/openapi.yaml
+++ b/here-naksha-app-service/src/main/resources/swagger/openapi.yaml
@@ -12,7 +12,7 @@ servers:
 info:
   title: "Naksha Hub-API"
   description: "Naksha Hub-API is a REST API to provide simple access to geo data."
-  version: "2.2.6"
+  version: "2.2.7"
 
 security:
   - AccessToken: [ ]
diff --git a/here-naksha-app-service/src/main/resources/test-config-with-extensions.json b/here-naksha-app-service/src/main/resources/test-config-with-extensions.json
index 1ffeb2100e..fd6a082b4a 100644
--- a/here-naksha-app-service/src/main/resources/test-config-with-extensions.json
+++ b/here-naksha-app-service/src/main/resources/test-config-with-extensions.json
@@ -5,6 +5,7 @@
   "httpPort": 8080,
   "env": "local",
   "requestBodyLimit": 25,
+  "requestHeaderLimit": 16,
   "authMode": "DUMMY",
   "jwtPvtKeyPath":  "auth/jwt.key",
   "jwtPubKeyPaths": "auth/jwt.pub",
diff --git a/here-naksha-app-service/src/main/resources/test-config.json b/here-naksha-app-service/src/main/resources/test-config.json
index b0e9d2f6d5..938c4bdba6 100644
--- a/here-naksha-app-service/src/main/resources/test-config.json
+++ b/here-naksha-app-service/src/main/resources/test-config.json
@@ -5,6 +5,7 @@
   "httpPort": 8080,
   "env": "local",
   "requestBodyLimit": 25,
+  "requestHeaderLimit": 16,
   "authMode": "DUMMY",
   "jwtPvtKeyPath":  "auth/jwt.key",
   "jwtPubKeyPaths": "auth/jwt.pub",
diff --git a/here-naksha-app-service/src/test/java/com/here/naksha/app/common/ApiTestMaintainer.java b/here-naksha-app-service/src/test/java/com/here/naksha/app/common/ApiTestMaintainer.java
index 54f6429356..8c0bc26bb8 100644
--- a/here-naksha-app-service/src/test/java/com/here/naksha/app/common/ApiTestMaintainer.java
+++ b/here-naksha-app-service/src/test/java/com/here/naksha/app/common/ApiTestMaintainer.java
@@ -20,20 +20,19 @@
 
 import static com.here.naksha.app.init.context.TestContextEntrypoint.loadTestContext;
 import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL;
-
-import com.here.naksha.app.init.context.ContainerTestContext;
 import com.here.naksha.app.init.context.TestContext;
-import org.junit.jupiter.api.extension.BeforeAllCallback;
-import org.junit.jupiter.api.extension.ExtensionContext;
+import com.here.naksha.app.service.NakshaApp;
+import org.junit.jupiter.api.extension.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+
 /**
  * Class responsible for once-per suite infrastructure build-up and tear-down.
  * <p>
  * See <a href="https://stackoverflow.com/a/51556718/7033439">this SO answer</a> for some context
  */
-public class ApiTestMaintainer implements BeforeAllCallback, ExtensionContext.Store.CloseableResource {
+public class ApiTestMaintainer implements BeforeAllCallback, ExtensionContext.Store.CloseableResource, ParameterResolver {
 
   private static final Logger log = LoggerFactory.getLogger(ApiTestMaintainer.class);
 
@@ -62,4 +61,19 @@ public void close() {
   private void registerCloseCallback(ExtensionContext context) {
     context.getRoot().getStore(GLOBAL).put(API_TEST_MAINTAINER_CONTEXT, this);
   }
+
+  /*
+    Using supportsParameter and resolveParameter functions, we inject running instance of NakshaApp into the eligible JUnits.
+    First function is to identify which JUnit test is interested in NakshaApp instance.
+    Second function is to actually inject the running instance of NakshaApp.
+  */
+  @Override
+  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
+    return parameterContext.isAnnotated(NakshaAppInjection.class) && parameterContext.getParameter().getType() == NakshaApp.class;
+  }
+
+  @Override
+  public NakshaApp resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
+    return TEST_CONTEXT.getNakshaAppInstance();
+  }
 }
diff --git a/here-naksha-app-service/src/test/java/com/here/naksha/app/common/NakshaAppInjection.java b/here-naksha-app-service/src/test/java/com/here/naksha/app/common/NakshaAppInjection.java
new file mode 100644
index 0000000000..d2f9fbbcfb
--- /dev/null
+++ b/here-naksha-app-service/src/test/java/com/here/naksha/app/common/NakshaAppInjection.java
@@ -0,0 +1,14 @@
+package com.here.naksha.app.common;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.FIELD,ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith({ApiTestMaintainer.class})
+public @interface NakshaAppInjection {
+}
diff --git a/here-naksha-app-service/src/test/java/com/here/naksha/app/init/context/TestContext.java b/here-naksha-app-service/src/test/java/com/here/naksha/app/init/context/TestContext.java
index 8cd5cd926b..f490ef19b9 100644
--- a/here-naksha-app-service/src/test/java/com/here/naksha/app/init/context/TestContext.java
+++ b/here-naksha-app-service/src/test/java/com/here/naksha/app/init/context/TestContext.java
@@ -8,6 +8,8 @@
 import com.here.naksha.app.service.NakshaApp;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
+
+import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -72,6 +74,8 @@ private void stopNaksha() {
     nakshaRef.get().stopInstance();
   }
 
+  public @Nullable NakshaApp getNakshaAppInstance() { return nakshaRef.get(); }
+
   enum State {
     NOT_STARTED,
     STARTED,
diff --git a/here-naksha-app-service/src/test/java/com/here/naksha/app/service/HeaderLimitTest.java b/here-naksha-app-service/src/test/java/com/here/naksha/app/service/HeaderLimitTest.java
new file mode 100644
index 0000000000..bcac327ef9
--- /dev/null
+++ b/here-naksha-app-service/src/test/java/com/here/naksha/app/service/HeaderLimitTest.java
@@ -0,0 +1,77 @@
+package com.here.naksha.app.service;
+
+import com.here.naksha.app.common.ApiTest;
+import com.here.naksha.app.common.NakshaAppInjection;
+import com.here.naksha.app.common.NakshaTestWebClient;
+import com.here.naksha.app.common.assertions.ResponseAssertions;
+import com.here.naksha.lib.hub.NakshaHubConfig;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.http.HttpResponse;
+import java.util.UUID;
+
+import static com.here.naksha.app.common.CommonApiTestSetup.setupSpaceAndRelatedResources;
+import static com.here.naksha.app.common.TestUtil.loadFileOrFail;
+
+public class HeaderLimitTest extends ApiTest {
+
+    private static final NakshaTestWebClient nakshaClient = new NakshaTestWebClient();
+
+    private static final String SPACE_ID = "header_limit_test_space";
+
+    @BeforeAll
+    static void setup() throws URISyntaxException, IOException, InterruptedException {
+        setupSpaceAndRelatedResources(nakshaClient, "HeaderLimit/setup");
+        final String initialFeaturesJson = loadFileOrFail("HeaderLimit/setup/create_features.json");
+        nakshaClient.post("hub/spaces/" + SPACE_ID + "/features", initialFeaturesJson, UUID.randomUUID().toString());
+    }
+
+    @Test
+    void tc001_testGetByIdsSuccess(final @NakshaAppInjection NakshaApp nakshaApp) throws Exception {
+        // Test API : GET /hub/spaces/{spaceId}/features
+        // Validate features getting returned for existing Ids when header size is within configured limit
+        // Given: Features By Ids request (against above space)
+        final String idsQueryParam = "id=my-custom-id-001" + "&id=my-custom-id-002";
+        final String expectedBodyPart =
+                loadFileOrFail("HeaderLimit/TC001_GetByIds/feature_response_part.json");
+        // When: Header size is just below limit (i.e. 100 bytes below limit)
+        final NakshaHubConfig config = nakshaApp.getHub().getConfig();
+        final String streamId = StringUtils.repeat("x", (config.requestHeaderLimit * 1024) - 100);
+
+        // When: Get Features request is submitted to NakshaHub Space Storage instance
+        HttpResponse<String> response = getNakshaClient().get("hub/spaces/" + SPACE_ID + "/features?" + idsQueryParam, streamId);
+
+        // Then: Perform assertions that the request is successful
+        ResponseAssertions.assertThat(response)
+                .hasStatus(200)
+                .hasStreamIdHeader(streamId)
+                .hasJsonBody(expectedBodyPart, "Get Feature response body doesn't match")
+                .hasUuids()
+        ;
+    }
+
+    @Test
+    void tc002_testGetByIdsWithHeaderLimitBreach(final @NakshaAppInjection NakshaApp nakshaApp) throws Exception {
+        // Test API : GET /hub/spaces/{spaceId}/features
+        // Validate Http error 413 when header size crosses the configured limit
+        // Given: Features By Ids request (against above space)
+        final String idsQueryParam = "id=my-custom-id-001" + "&id=my-custom-id-002";
+        final String expectedBodyPart =
+                loadFileOrFail("HeaderLimit/TC001_GetByIds/feature_response_part.json");
+        // When: Header size breaches the limit
+        final NakshaHubConfig config = nakshaApp.getHub().getConfig();
+        final String streamId = StringUtils.repeat("x", config.requestHeaderLimit * 1024);
+
+        // When: Get Features request is submitted to NakshaHub Space Storage instance
+        HttpResponse<String> response = getNakshaClient().get("hub/spaces/" + SPACE_ID + "/features?" + idsQueryParam, streamId);
+
+        // Then: Perform assertions that the request fails with header limit breach
+        ResponseAssertions.assertThat(response)
+                .hasStatus(431);
+    }
+
+}
diff --git a/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/TC001_GetByIds/feature_response_part.json b/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/TC001_GetByIds/feature_response_part.json
new file mode 100644
index 0000000000..ce94789fdb
--- /dev/null
+++ b/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/TC001_GetByIds/feature_response_part.json
@@ -0,0 +1,35 @@
+{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "id": "my-custom-id-001",
+      "type": "Feature",
+      "properties": {
+        "speedLimit": "60",
+        "filteredOut": "yes",
+        "@ns:com:here:xyz": {
+          "tags": [ "selection-test" ]
+        }
+      },
+      "geometry": {
+        "type":"Point",
+        "coordinates":[ 8.68872, 50.0561, 292.94377758]
+      }
+    },
+    {
+      "id": "my-custom-id-002",
+      "type": "Feature",
+      "properties": {
+        "speedLimit": "80",
+        "filteredOut": "yes",
+        "@ns:com:here:xyz": {
+          "tags": [ "selection-test" ]
+        }
+      },
+      "geometry": {
+        "type":"Point",
+        "coordinates":[ 9.68872, 51.0561, 293.94377758]
+      }
+    }
+  ]
+}
diff --git a/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_event_handler.json b/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_event_handler.json
new file mode 100644
index 0000000000..6807431b30
--- /dev/null
+++ b/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_event_handler.json
@@ -0,0 +1,12 @@
+{
+  "id": "header_limit_test_handler",
+  "type": "EventHandler",
+  "title": "Storage Handler for UniMap Moderation Dev Storage",
+  "description": "Default Naksha Storage Handler for operations on UniMap Moderation Dev Storage",
+  "className": "com.here.naksha.lib.handlers.DefaultStorageHandler",
+  "active": true,
+  "extensionId": null,
+  "properties": {
+    "storageId": "header_limit_test_storage"
+  }
+}
\ No newline at end of file
diff --git a/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_features.json b/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_features.json
new file mode 100644
index 0000000000..ce94789fdb
--- /dev/null
+++ b/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_features.json
@@ -0,0 +1,35 @@
+{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "id": "my-custom-id-001",
+      "type": "Feature",
+      "properties": {
+        "speedLimit": "60",
+        "filteredOut": "yes",
+        "@ns:com:here:xyz": {
+          "tags": [ "selection-test" ]
+        }
+      },
+      "geometry": {
+        "type":"Point",
+        "coordinates":[ 8.68872, 50.0561, 292.94377758]
+      }
+    },
+    {
+      "id": "my-custom-id-002",
+      "type": "Feature",
+      "properties": {
+        "speedLimit": "80",
+        "filteredOut": "yes",
+        "@ns:com:here:xyz": {
+          "tags": [ "selection-test" ]
+        }
+      },
+      "geometry": {
+        "type":"Point",
+        "coordinates":[ 9.68872, 51.0561, 293.94377758]
+      }
+    }
+  ]
+}
diff --git a/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_space.json b/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_space.json
new file mode 100644
index 0000000000..530fd8c671
--- /dev/null
+++ b/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_space.json
@@ -0,0 +1,15 @@
+{
+  "id": "header_limit_test_space",
+  "type": "Space",
+  "title": "Temp Space for Testing Header Limit",
+  "description": "Temp Space for validating header limit specific test scenarios",
+  "eventHandlerIds": [
+    "header_limit_test_handler"
+  ],
+  "properties": {
+    "collection": {
+      "id": "header_limit_test_collection",
+      "type": "NakshaCollection"
+    }
+  }
+}
\ No newline at end of file
diff --git a/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_storage.json b/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_storage.json
new file mode 100644
index 0000000000..112b9537d1
--- /dev/null
+++ b/here-naksha-app-service/src/test/resources/unit_test_data/HeaderLimit/setup/create_storage.json
@@ -0,0 +1,19 @@
+{
+  "id": "header_limit_test_storage",
+  "type": "Storage",
+  "title": "Test PSQL storage",
+  "description": "PSQL storage instance for testing purpose",
+  "className": "com.here.naksha.lib.psql.PsqlStorage",
+  "properties": {
+    "master": {
+      "host": "${dataDb.host}",
+      "db": "${dataDb.db}",
+      "port": ${dataDb.port},
+      "user": "${dataDb.user}",
+      "password": "${dataDb.password}",
+      "readOnly": false
+    },
+    "appName": "test-app-name",
+    "schema": "${dataDb.schema}"
+  }
+}
diff --git a/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java
index 3ac844d81b..4ce4abd585 100644
--- a/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java
+++ b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java
@@ -64,12 +64,13 @@ public class NakshaVersion implements Comparable<NakshaVersion> {
   public static final String v2_2_4 = "2.2.4";
   public static final String v2_2_5 = "2.2.5";
   public static final String v2_2_6 = "2.2.6";
+  public static final String v2_2_7 = "2.2.7";
 
   /**
    * The latest version of the naksha-extension stored in the resources.
    */
   @AvailableSince(v2_0_5)
-  public static final NakshaVersion latest = of(v2_2_6);
+  public static final NakshaVersion latest = of(v2_2_7);
 
   private final int major;
   private final int minor;
diff --git a/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java b/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java
index eab00743d6..b7e0a22e90 100644
--- a/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java
+++ b/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java
@@ -57,10 +57,20 @@ public final class NakshaHubConfig extends XyzFeature implements JsonSerializabl
   public static final Integer DEF_REQ_BODY_LIMIT = 25;
 
   /**
-   * The maximum Http request body limit in MB.
+   * The maximum supported Http request body limit in MB.
    */
   public static final Integer MAX_REQ_BODY_LIMIT = Math.max(25, DEF_REQ_BODY_LIMIT);
 
+  /**
+   * The default Http request header limit in KB.
+   */
+  public static final Integer DEF_REQ_HEADER_LIMIT = 8;
+
+  /**
+   * The maximum supported Http request header limit in KB.
+   */
+  public static final Integer MAX_REQ_HEADER_LIMIT = Math.max(16, DEF_REQ_HEADER_LIMIT);
+
   /**
    * Returns a default application name used at many placed.
    *
@@ -120,6 +130,7 @@ public final class NakshaHubConfig extends XyzFeature implements JsonSerializabl
       @JsonProperty("storageParams") @Nullable Map<String, Object> storageParams,
       @JsonProperty("extensionConfigParams") @Nullable ExtensionConfigParams extensionConfigParams,
       @JsonProperty("requestBodyLimit") @Nullable Integer requestBodyLimit,
+      @JsonProperty("requestHeaderLimit") @Nullable Integer requestHeaderLimit,
       @JsonProperty("maxParallelRequestsPerCPU") @Nullable Integer maxParallelRequestsPerCPU,
       @JsonProperty("maxPctParallelRequestsPerActor") @Nullable Integer maxPctParallelRequestsPerActor) {
     super(id);
@@ -203,15 +214,26 @@ public final class NakshaHubConfig extends XyzFeature implements JsonSerializabl
     this.extensionConfigParams = extensionConfigParams;
     if (requestBodyLimit == null) {
       this.requestBodyLimit = DEF_REQ_BODY_LIMIT;
-    } else if (requestBodyLimit > MAX_REQ_BODY_LIMIT) {
+    } else if (requestBodyLimit <= 0 || requestBodyLimit > MAX_REQ_BODY_LIMIT) {
       logger.warn(
-          "Configured request body limit {} MB not supported. Falling back to default limit of {} MB",
+          "Configured request body limit {} MB not supported. Falling back to max limit of {} MB",
           requestBodyLimit,
-          DEF_REQ_BODY_LIMIT);
-      this.requestBodyLimit = DEF_REQ_BODY_LIMIT;
+          MAX_REQ_BODY_LIMIT);
+      this.requestBodyLimit = MAX_REQ_BODY_LIMIT;
     } else {
       this.requestBodyLimit = requestBodyLimit;
     }
+    if (requestHeaderLimit == null) {
+      this.requestHeaderLimit = DEF_REQ_HEADER_LIMIT;
+    } else if (requestHeaderLimit <= 0 || requestHeaderLimit > MAX_REQ_HEADER_LIMIT) {
+      logger.warn(
+          "Configured request header limit {} KB not supported. Falling back to max limit of {} KB",
+          requestHeaderLimit,
+          MAX_REQ_HEADER_LIMIT);
+      this.requestHeaderLimit = MAX_REQ_HEADER_LIMIT;
+    } else {
+      this.requestHeaderLimit = requestHeaderLimit;
+    }
     this.maxParallelRequestsPerCPU =
         maxParallelRequestsPerCPU != null ? maxParallelRequestsPerCPU : defaultMaxParallelRequestsPerCPU();
     this.maxPctParallelRequestsPerActor = maxPctParallelRequestsPerActor != null
@@ -415,6 +437,11 @@ public static int defaultMaxPctParallelRequestsPerActor() {
    */
   public final Integer requestBodyLimit;
 
+  /**
+   * Optional Http request header limit in KB. Default is {@link #DEF_REQ_HEADER_LIMIT}.
+   */
+  public final @NotNull Integer requestHeaderLimit;
+
   /**
    * Optional Total Concurrency Limit
    */
diff --git a/here-naksha-lib-hub/src/main/resources/config/default-config.json b/here-naksha-lib-hub/src/main/resources/config/default-config.json
index 663e1dfc6a..bdeec9228d 100644
--- a/here-naksha-lib-hub/src/main/resources/config/default-config.json
+++ b/here-naksha-lib-hub/src/main/resources/config/default-config.json
@@ -5,6 +5,7 @@
   "httpPort": 8080,
   "env": "local",
   "requestBodyLimit": 25,
+  "requestHeaderLimit": 8,
   "authMode": "JWT",
   "jwtName": "jwt",
   "maintenanceInitialDelayInMins": 60,


Reviews:
user: login: hirenkp2000
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T13:33:27Z
user: login: rlakde
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T14:02:10Z
user: login: rlakde
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-07T14:04:01Z
user: login: xeus2001
body: Nothing critical from my side, but you may consider the two comments I made.
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-07T14:14:08Z
user: login: hirenkp2000
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T16:36:46Z

Review comments:
diff_hunk: @@ -47,3 +47,8 @@ excludes:
   - pattern: "provided"
     reason: "PROVIDED_DEPENDENCY_OF"
     comment: "Scope with dependencies provided by the JDK or container at runtime. Not included in released artifacts."
+analyzer:
path: .ort.yml
user: login: hirenkp2000
body: just a note that this change is to fix OSS scanner issue; not related to this PR; but merged here to avoid another PR
created_at: 2025-04-07T13:33:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,77 @@
+package com.here.naksha.app.service;
+
+import com.here.naksha.app.common.ApiTest;
+import com.here.naksha.app.common.NakshaAppInjection;
+import com.here.naksha.app.common.NakshaTestWebClient;
+import com.here.naksha.app.common.assertions.ResponseAssertions;
+import com.here.naksha.lib.hub.NakshaHubConfig;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.http.HttpResponse;
+import java.util.UUID;
+
+import static com.here.naksha.app.common.CommonApiTestSetup.setupSpaceAndRelatedResources;
+import static com.here.naksha.app.common.TestUtil.loadFileOrFail;
+
+public class HeaderLimitTest extends ApiTest {
+
+    private static final NakshaTestWebClient nakshaClient = new NakshaTestWebClient();
+
+    private static final String SPACE_ID = "header_limit_test_space";
+
+    @BeforeAll
+    static void setup() throws URISyntaxException, IOException, InterruptedException {
+        setupSpaceAndRelatedResources(nakshaClient, "HeaderLimit/setup");
+        final String initialFeaturesJson = loadFileOrFail("HeaderLimit/setup/create_features.json");
+        nakshaClient.post("hub/spaces/" + SPACE_ID + "/features", initialFeaturesJson, UUID.randomUUID().toString());
+    }
+
+    @Test
+    void tc001_testGetByIdsSuccess(final @NakshaAppInjection NakshaApp nakshaApp) throws Exception {
+        // Test API : GET /hub/spaces/{spaceId}/features
+        // Validate features getting returned for existing Ids when header size is within configured limit
+        // Given: Features By Ids request (against above space)
+        final String idsQueryParam = "id=my-custom-id-001" + "&id=my-custom-id-002";
+        final String expectedBodyPart =
+                loadFileOrFail("HeaderLimit/TC001_GetByIds/feature_response_part.json");
+        // When: Header size is just below limit (i.e. 100 bytes below limit)
+        final NakshaHubConfig config = nakshaApp.getHub().getConfig();
+        final String streamId = StringUtils.repeat("x", (config.requestHeaderLimit * 1024) - 100);
+
+        // When: Get Features request is submitted to NakshaHub Space Storage instance
+        HttpResponse<String> response = getNakshaClient().get("hub/spaces/" + SPACE_ID + "/features?" + idsQueryParam, streamId);
+
+        // Then: Perform assertions that the request is successful
+        ResponseAssertions.assertThat(response)
+                .hasStatus(200)
+                .hasStreamIdHeader(streamId)
+                .hasJsonBody(expectedBodyPart, "Get Feature response body doesn't match")
+                .hasUuids()
+        ;
+    }
+
+    @Test
+    void tc002_testGetByIdsWithHeaderLimitBreach(final @NakshaAppInjection NakshaApp nakshaApp) throws Exception {
+        // Test API : GET /hub/spaces/{spaceId}/features
+        // Validate Http error 413 when header size crosses the configured limit
+        // Given: Features By Ids request (against above space)
+        final String idsQueryParam = "id=my-custom-id-001" + "&id=my-custom-id-002";
+        final String expectedBodyPart =
+                loadFileOrFail("HeaderLimit/TC001_GetByIds/feature_response_part.json");
+        // When: Header size is just below limit (i.e. 100 bytes below limit)
path: here-naksha-app-service/src/test/java/com/here/naksha/app/service/HeaderLimitTest.java
user: login: rlakde
body: Should the below comment be updated to say that the header size is exactly at the limit?
`When: Header size is just below limit (i.e. 100 bytes below limit)`
created_at: 2025-04-07T14:02:10Z
author_association: MEMBER
diff_hunk: @@ -212,6 +223,17 @@ public final class NakshaHubConfig extends XyzFeature implements JsonSerializabl
     } else {
       this.requestBodyLimit = requestBodyLimit;
     }
+    if (requestHeaderLimit == null) {
+      this.requestHeaderLimit = DEF_REQ_HEADER_LIMIT;
+    } else if (requestHeaderLimit > MAX_REQ_HEADER_LIMIT) {
+      logger.warn(
+          "Configured request header limit {} KB not supported. Falling back to default limit of {} KB",
+          requestHeaderLimit,
+          DEF_REQ_HEADER_LIMIT);
+      this.requestHeaderLimit = DEF_REQ_HEADER_LIMIT;
path: here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java
user: login: xeus2001
body: Why do we not use the largest value `MAX_REQ_HEADER_LIMIT`? I mean, if I ask for 10MB, the max is 9MB and default 10KB, i expect to get 9MB or an exception, but getting 10KB is IMHO the most unexpected behavior.
created_at: 2025-04-07T14:04:28Z
author_association: MEMBER
diff_hunk: @@ -212,6 +223,17 @@ public final class NakshaHubConfig extends XyzFeature implements JsonSerializabl
     } else {
       this.requestBodyLimit = requestBodyLimit;
     }
+    if (requestHeaderLimit == null) {
+      this.requestHeaderLimit = DEF_REQ_HEADER_LIMIT;
+    } else if (requestHeaderLimit > MAX_REQ_HEADER_LIMIT) {
+      logger.warn(
+          "Configured request header limit {} KB not supported. Falling back to default limit of {} KB",
+          requestHeaderLimit,
+          DEF_REQ_HEADER_LIMIT);
+      this.requestHeaderLimit = DEF_REQ_HEADER_LIMIT;
+    } else {
+      this.requestHeaderLimit = requestHeaderLimit;
path: here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java
user: login: xeus2001
body: What happens when a negative value is provided?
created_at: 2025-04-07T14:04:46Z
author_association: MEMBER
diff_hunk: @@ -212,6 +223,17 @@ public final class NakshaHubConfig extends XyzFeature implements JsonSerializabl
     } else {
       this.requestBodyLimit = requestBodyLimit;
     }
+    if (requestHeaderLimit == null) {
+      this.requestHeaderLimit = DEF_REQ_HEADER_LIMIT;
+    } else if (requestHeaderLimit > MAX_REQ_HEADER_LIMIT) {
+      logger.warn(
+          "Configured request header limit {} KB not supported. Falling back to default limit of {} KB",
+          requestHeaderLimit,
+          DEF_REQ_HEADER_LIMIT);
+      this.requestHeaderLimit = DEF_REQ_HEADER_LIMIT;
+    } else {
+      this.requestHeaderLimit = requestHeaderLimit;
path: here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java
user: login: hirenkp2000
body: it is us only who change this configuration (very rarely) so extremes (like negative) are less likely to be the case... nevertheless, will add a check
created_at: 2025-04-07T16:36:46Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-07T12:25:42Z
body: ### Code Coverage
|Overall Project|42.48% **`-0.03%`**|:green_apple:|
|:-|:-|:-:|
|Files changed|67.61%|:green_apple:|
<br>

|Module|Coverage||
|:-|:-|:-:|
|here-naksha-app-service|71.37%|:green_apple:|
|here-naksha-lib-core|32.69%|:green_apple:|
|here-naksha-lib-hub|24.86% **`-0.91%`**|:green_apple:|

<details>
<summary>Files</summary>

|Module|File|Coverage||
|:-|:-|:-|:-:|
|here-naksha-app-service|[NakshaHttpVerticle.java](https://github.com/heremaps/naksha/blob/7612bdbd58c998a165b93060a0ea1a92ff83268e/here-naksha-app-service%2Fsrc%2Fmain%2Fjava%2Fcom%2Fhere%2Fnaksha%2Fapp%2Fservice%2Fhttp%2FNakshaHttpVerticle.java)|61.5%|:green_apple:|
|here-naksha-lib-core|[NakshaVersion.java](https://github.com/heremaps/naksha/blob/7612bdbd58c998a165b93060a0ea1a92ff83268e/here-naksha-lib-core%2Fsrc%2Fmain%2Fjava%2Fcom%2Fhere%2Fnaksha%2Flib%2Fcore%2FNakshaVersion.java)|42.31%|:green_apple:|
|here-naksha-lib-hub|[NakshaHubConfig.java](https://github.com/heremaps/naksha/blob/7612bdbd58c998a165b93060a0ea1a92ff83268e/here-naksha-lib-hub%2Fsrc%2Fmain%2Fjava%2Fcom%2Fhere%2Fnaksha%2Flib%2Fhub%2FNakshaHubConfig.java)|6.75% **`-5.97%`**|:green_apple:|

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-07T13:19:14Z
body: ### Code Coverage
|Overall Project|42.47% **`-0.03%`**|:green_apple:|
|:-|:-|:-:|
|Files changed|67.61%|:green_apple:|
<br>

|Module|Coverage||
|:-|:-|:-:|
|here-naksha-app-service|71.37%|:green_apple:|
|here-naksha-lib-core|32.68%|:green_apple:|
|here-naksha-lib-hub|24.86% **`-0.91%`**|:green_apple:|

<details>
<summary>Files</summary>

|Module|File|Coverage||
|:-|:-|:-|:-:|
|here-naksha-app-service|[NakshaHttpVerticle.java](https://github.com/heremaps/naksha/blob/0b94c674e1faf1e3af9005f8275946fd8b77b002/here-naksha-app-service%2Fsrc%2Fmain%2Fjava%2Fcom%2Fhere%2Fnaksha%2Fapp%2Fservice%2Fhttp%2FNakshaHttpVerticle.java)|61.5%|:green_apple:|
|here-naksha-lib-core|[NakshaVersion.java](https://github.com/heremaps/naksha/blob/0b94c674e1faf1e3af9005f8275946fd8b77b002/here-naksha-lib-core%2Fsrc%2Fmain%2Fjava%2Fcom%2Fhere%2Fnaksha%2Flib%2Fcore%2FNakshaVersion.java)|42.31%|:green_apple:|
|here-naksha-lib-hub|[NakshaHubConfig.java](https://github.com/heremaps/naksha/blob/0b94c674e1faf1e3af9005f8275946fd8b77b002/here-naksha-lib-hub%2Fsrc%2Fmain%2Fjava%2Fcom%2Fhere%2Fnaksha%2Flib%2Fhub%2FNakshaHubConfig.java)|6.75% **`-5.97%`**|:green_apple:|

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-07T16:40:52Z
body: ### Code Coverage
|Overall Project|42.46% **`-0.05%`**|:green_apple:|
|:-|:-|:-:|
|Files changed|55.81%|:green_apple:|
<br>

|Module|Coverage||
|:-|:-|:-:|
|here-naksha-app-service|71.37%|:green_apple:|
|here-naksha-lib-core|32.68%|:green_apple:|
|here-naksha-lib-hub|24.8% **`-1.5%`**|:green_apple:|

<details>
<summary>Files</summary>

|Module|File|Coverage||
|:-|:-|:-|:-:|
|here-naksha-app-service|[NakshaHttpVerticle.java](https://github.com/heremaps/naksha/blob/69fb845d58c9dbc4e729552d1b29738c23c854b8/here-naksha-app-service%2Fsrc%2Fmain%2Fjava%2Fcom%2Fhere%2Fnaksha%2Fapp%2Fservice%2Fhttp%2FNakshaHttpVerticle.java)|61.5%|:green_apple:|
|here-naksha-lib-core|[NakshaVersion.java](https://github.com/heremaps/naksha/blob/69fb845d58c9dbc4e729552d1b29738c23c854b8/here-naksha-lib-core%2Fsrc%2Fmain%2Fjava%2Fcom%2Fhere%2Fnaksha%2Flib%2Fcore%2FNakshaVersion.java)|42.31%|:green_apple:|
|here-naksha-lib-hub|[NakshaHubConfig.java](https://github.com/heremaps/naksha/blob/69fb845d58c9dbc4e729552d1b29738c23c854b8/here-naksha-lib-hub%2Fsrc%2Fmain%2Fjava%2Fcom%2Fhere%2Fnaksha%2Flib%2Fhub%2FNakshaHubConfig.java)|6.65% **`-9.72%`**|:green_apple:|

</details>
author_association: NONE