diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a9c322957c0..9c584064dd9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -140,10 +140,11 @@ jobs:
uses: ./.github/actions/integration-test-setup
- name: Run base tests
+ # enable the http-optimized-serializers feature for the old testsuite to verify it works as expected
run: |
TESTS=`testsuite/integration-arquillian/tests/base/testsuites/base-suite.sh ${{ matrix.group }}`
echo "Tests: $TESTS"
- ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh
+ ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dauth.server.feature=http-optimized-serializers -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh
- uses: ./.github/actions/upload-flaky-tests
name: Upload flaky tests
diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java
index 56f5b186a0c..48e56d0bc24 100755
--- a/common/src/main/java/org/keycloak/common/Profile.java
+++ b/common/src/main/java/org/keycloak/common/Profile.java
@@ -148,6 +148,8 @@ public class Profile {
DB_TIDB("TiDB database type", Type.EXPERIMENTAL),
+ HTTP_OPTIMIZED_SERIALIZERS("Optimized JSON serializers for better performance of the HTTP layer", Type.PREVIEW),
+
/**
* @see Deprecate for removal the Instagram social broker.
*/
diff --git a/docs/documentation/release_notes/index.adoc b/docs/documentation/release_notes/index.adoc
index 7c5a58ebddf..95f34390fff 100644
--- a/docs/documentation/release_notes/index.adoc
+++ b/docs/documentation/release_notes/index.adoc
@@ -13,6 +13,9 @@ include::topics/templates/document-attributes.adoc[]
:release_header_latest_link: {releasenotes_link_latest}
include::topics/templates/release-header.adoc[]
+== {project_name_full} 26.5.0
+include::topics/26_5_0.adoc[leveloffset=2]
+
== {project_name_full} 26.4.0
include::topics/26_4_0.adoc[leveloffset=2]
diff --git a/docs/documentation/release_notes/topics/26_5_0.adoc b/docs/documentation/release_notes/topics/26_5_0.adoc
index 6e6013a4db0..085ddcafaee 100644
--- a/docs/documentation/release_notes/topics/26_5_0.adoc
+++ b/docs/documentation/release_notes/topics/26_5_0.adoc
@@ -1,9 +1,22 @@
// Release notes should contain only headline-worthy new features,
// assuming that people who migrate will read the upgrading guide anyway.
-//
-== Breaking Fix for Windows in Loopback Hostname Verification
+
+= Preview of enhanced HTTP performance
+
+You can now enable a more efficient way to handle JSON data in the HTTP layer.
+This change increases throughput by ~5%, stabilizes response times, and reduces system resource usage.
+
+In order to apply it, you need to explicitly enable the feature `http-optimized-serializers`.
+
+NOTE: This feature is *preview*.
+ifeval::[{project_community}==true]
+We gather more feedback about potential issues in https://github.com/keycloak/keycloak/discussions/43484[this discussion]. We appreciate any feedback.
+endif::[]
+
+For more details, see the https://www.keycloak.org/server/configuration-production[Configuring Keycloak for production] guide.
+
+= Breaking Fix for Windows in Loopback Hostname Verification
This release introduces a breaking change for Windows users: setups that previously relied on custom machine names or non-standard hostnames for loopback (e.g., `127.0.0.1` resolving to a custom name) may require updates to their trusted domain configuration. Only `localhost` and `*.localhost` are now recognized for loopback verification.
Keycloak now consistently normalizes loopback addresses to `localhost` for domain verification across all platforms. This change ensures predictable behavior for trusted domain checks, regardless of the underlying OS.
-
diff --git a/docs/guides/securing-apps/token-exchange.adoc b/docs/guides/securing-apps/token-exchange.adoc
index 8f31018ad82..44cfd71ff42 100644
--- a/docs/guides/securing-apps/token-exchange.adoc
+++ b/docs/guides/securing-apps/token-exchange.adoc
@@ -1,5 +1,6 @@
<#import "/templates/guide.adoc" as tmpl>
<#import "/templates/links.adoc" as links>
+<#import "/templates/features.adoc" as features>
<@tmpl.guide
title="Configuring and using token exchange"
@@ -327,22 +328,7 @@ s|Subject impersonation (including direct naked impersonation) | Not implement
[[_legacy-token-exchange]]
== Legacy token exchange
-:tech_feature_name: Token Exchange
-:tech_feature_id: token-exchange
-
-[NOTE]
-====
-{tech_feature_name} is
-*Preview*
-and is not fully supported. This feature is disabled by default.
-
-To enable start the server with `--features=preview`
-ifdef::tech_feature_id[]
-or `--features={tech_feature_id}`
-endif::[]
-
-{tech_feature_name} is *Technology Preview* and is not fully supported.
-====
+<@features.techpreview feature="token-exchange"/>
[NOTE]
====
diff --git a/docs/guides/server/configuration-production.adoc b/docs/guides/server/configuration-production.adoc
index f752181d281..166ba6e546e 100644
--- a/docs/guides/server/configuration-production.adoc
+++ b/docs/guides/server/configuration-production.adoc
@@ -1,6 +1,7 @@
<#import "/templates/guide.adoc" as tmpl>
<#import "/templates/kc.adoc" as kc>
<#import "/templates/links.adoc" as links>
+<#import "/templates/features.adoc" as features>
<@tmpl.guide
title="Configuring {project_name} for production"
@@ -87,4 +88,25 @@ export JAVA_OPTS_APPEND="-Djava.net.preferIPv4Stack=false -Djava.net.preferIPv6A
See <@links.server id="caching" anchor="network-bind-address"/> for more details.
+== Preview of enhanced HTTP performance
+<@features.techpreview feature="http-optimized-serializers" additionalCommunityText="We gather more feedback on this feature to promote it to supported. Please, share your feedback about any issue in https://github.com/keycloak/keycloak/discussions/43484[this discussion]."/>
+
+In production environments, the performance of the HTTP layer is critical.
+Every request passes through it, making it a key factor in overall system responsiveness, scalability, and user experience.
+
+This feature improves how {project_name} handles JSON data in HTTP requests and responses.
+The result is a more efficient runtime with measurable benefits:
+
+- ~5% increase in throughput
+- More stable response times
+- Reduced system resource usage
+
+These improvements help ensure smoother, more predictable performance at scale while also lowering the operational cost of running production systems.
+
+The only known tradeoff is that build time increases by ~6% as certain actions were moved to the build time instead of runtime.
+
+You can enable this feature as follows:
+
+<@kc.start parameters="--features=http-optimized-serializers"/>
+
@tmpl.guide>
diff --git a/docs/guides/templates/features.adoc b/docs/guides/templates/features.adoc
index 4090f84a2ee..2713753ea9c 100644
--- a/docs/guides/templates/features.adoc
+++ b/docs/guides/templates/features.adoc
@@ -8,3 +8,26 @@
#list>
|===
#macro>
+
+<#macro techpreview feature additionalCommunityText="">
+<#assign profileFeature = ctx.features.getFeature(feature)>
+
+[NOTE]
+====
+${profileFeature.description} is
+ifeval::[{project_product}==true]
+*Technology Preview*
+endif::[]
+ifeval::[{project_community}==true]
+*Preview*
+endif::[]
+and is not fully supported. This feature is disabled by default.
+
+ifeval::[{project_community}==true]
+${additionalCommunityText!""}
+endif::[]
+
+To enable start the server with <#if profileFeature.type != "PREVIEW_DISABLED_BY_DEFAULT">`--features=preview` or #if>`--features=${profileFeature.name}`
+
+====
+#macro>
diff --git a/docs/maven-plugin/src/main/java/org/keycloak/guides/maven/Features.java b/docs/maven-plugin/src/main/java/org/keycloak/guides/maven/Features.java
index b34e31a19c8..160d3addc98 100644
--- a/docs/maven-plugin/src/main/java/org/keycloak/guides/maven/Features.java
+++ b/docs/maven-plugin/src/main/java/org/keycloak/guides/maven/Features.java
@@ -43,6 +43,11 @@ public class Features {
return features.stream().filter(f -> f.profileFeature.getUpdatePolicy() == Profile.FeatureUpdatePolicy.ROLLING_NO_UPGRADE).collect(Collectors.toList());
}
+ public Feature getFeature(String featureId) {
+ return features.stream().filter(f -> f.getName().equals(featureId)).findAny()
+ .orElseThrow(() -> new IllegalArgumentException("Cannot find the '%s' feature for guides".formatted(featureId)));
+ }
+
public static class Feature {
private final Profile.Feature profileFeature;
@@ -67,7 +72,7 @@ public class Features {
return profileFeature.getUpdatePolicy().toString();
}
- private Profile.Feature.Type getType() {
+ public Profile.Feature.Type getType() {
return profileFeature.getType();
}
}
diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/HttpOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/HttpOptions.java
index 474b69ac5e3..45504c2b2f0 100644
--- a/quarkus/config-api/src/main/java/org/keycloak/config/HttpOptions.java
+++ b/quarkus/config-api/src/main/java/org/keycloak/config/HttpOptions.java
@@ -150,5 +150,4 @@ public class HttpOptions {
.description("Service level objectives for HTTP server requests. Use this instead of the default histogram, or use it in combination to add additional buckets. " +
"Specify a list of comma-separated values defined in milliseconds. Example with buckets from 5ms to 10s: 5,10,25,50,250,500,1000,2500,5000,10000")
.build();
-
}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java
index 3558d12d575..8bc0e7d55c0 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java
@@ -4,6 +4,7 @@ import io.quarkus.runtime.util.ClassPathUtils;
import io.quarkus.vertx.http.runtime.options.TlsUtils;
import io.smallrye.config.ConfigSourceInterceptorContext;
+import org.keycloak.common.Profile;
import org.keycloak.common.crypto.FipsMode;
import org.keycloak.config.HttpOptions;
import org.keycloak.config.SecurityOptions;
@@ -23,6 +24,7 @@ import java.util.Optional;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalValue;
+import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromFeature;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
public final class HttpPropertyMappers implements PropertyMapperGrouping {
@@ -154,6 +156,9 @@ public final class HttpPropertyMappers implements PropertyMapperGrouping {
fromOption(HttpOptions.HTTP_METRICS_SLOS)
.isEnabled(MetricsPropertyMappers::metricsEnabled, MetricsPropertyMappers.METRICS_ENABLED_MSG)
.paramLabel("list of buckets")
+ .build(),
+ fromFeature(Profile.Feature.HTTP_OPTIMIZED_SERIALIZERS)
+ .to("quarkus.rest.jackson.optimization.enable-reflection-free-serializers")
.build()
);
}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java
index b561aad2bfc..50c5653b77e 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java
@@ -33,8 +33,10 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
+import org.keycloak.common.Profile;
import org.keycloak.config.DeprecatedMetadata;
import org.keycloak.config.Option;
+import org.keycloak.config.OptionBuilder;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.cli.ShortErrorMessageHandler;
@@ -547,6 +549,22 @@ public class PropertyMapper {
return new PropertyMapper.Builder<>(opt);
}
+ /**
+ * Create a property mapper from a feature.
+ * The mapper maps to external properties the state of the feature.
+ *
+ * If the feature is enabled, it returns {@code true}. Otherwise {@code null}.
+ */
+ public static PropertyMapper.Builder fromFeature(Profile.Feature feature) {
+ final var option = new OptionBuilder<>(feature.getKey() + "-hidden-mapper", Boolean.class)
+ .buildTime(true)
+ .hidden()
+ .build();
+ return new Builder<>(option)
+ .isEnabled(() -> Profile.isFeatureEnabled(feature))
+ .transformer((v, ctx) -> Boolean.TRUE.toString()); // we know the feature is enabled due to .isEnabled()
+ }
+
public void validate(ConfigValue value) {
if (validator != null) {
validator.accept(this, value);
diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java
index ee62b76f839..5a2e32f5eb8 100644
--- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java
+++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java
@@ -966,4 +966,16 @@ public class PicocliTest extends AbstractConfigurationTest {
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Available only when health is enabled"));
}
+
+ @Test
+ public void httpOptimizedSerializers() {
+ var nonRunningPicocli = pseudoLaunch("start-dev");
+ assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
+ assertExternalConfigNull("quarkus.rest.jackson.optimization.enable-reflection-free-serializers");
+ onAfter();
+
+ nonRunningPicocli = pseudoLaunch("start-dev", "--features=http-optimized-serializers");
+ assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
+ assertExternalConfig("quarkus.rest.jackson.optimization.enable-reflection-free-serializers", "true");
+ }
}