High severity8.3NVD Advisory· Published Feb 13, 2025· Updated Apr 15, 2026
CVE-2025-1247
CVE-2025-1247
Description
A flaw was found in Quarkus REST that allows request parameters to leak between concurrent requests if endpoints use field injection without a CDI scope. This vulnerability allows attackers to manipulate request data, impersonate users, or access sensitive information.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.quarkus:quarkus-restMaven | >= 3.16.0.CR1, < 3.18.2 | 3.18.2 |
io.quarkus:quarkus-rest-deploymentMaven | >= 3.16.0.CR1, < 3.18.2 | 3.18.2 |
io.quarkus:quarkus-restMaven | >= 3.9.0.CR1, < 3.15.3.1 | 3.15.3.1 |
io.quarkus:quarkus-rest-deploymentMaven | >= 3.9.0.CR1, < 3.15.3.1 | 3.15.3.1 |
io.quarkus:quarkus-restMaven | < 3.8.6.1 | 3.8.6.1 |
io.quarkus:quarkus-rest-deploymentMaven | < 3.8.6.1 | 3.8.6.1 |
Patches
3d8df15cec17dMerge pull request #46178 from geoand/3.8-#45789
14 files changed · +522 −3
extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusServerEndpointIndexer.java+23 −0 modified@@ -8,6 +8,8 @@ import java.util.Map; import java.util.function.Predicate; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.spi.DeploymentException; import jakarta.ws.rs.core.MediaType; import org.jboss.jandex.AnnotationInstance; @@ -28,6 +30,7 @@ import org.jboss.resteasy.reactive.server.processor.ServerIndexedParameter; import org.jboss.resteasy.reactive.server.spi.EndpointInvokerFactory; +import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.builder.BuildException; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; @@ -274,4 +277,24 @@ protected void warnAboutMissUsedBodyParameter(DotName httpMethod, MethodInfo met super.warnAboutMissUsedBodyParameter(httpMethod, methodInfo); } + /** + * At this point we know exactly which resources will require field injection and therefore are required to be + * {@link RequestScoped}. + * We can't change anything CDI related at this point (because it would create build cycles), so all we can do + * is fail the build if the resource has not already been handled automatically (by the best effort approach performed + * elsewhere) + * or it's not manually set to be {@link RequestScoped}. + */ + @Override + protected void verifyClassThatRequiresFieldInjection(ClassInfo classInfo) { + if (!alreadyHandledRequestScopedResources.contains(classInfo.name())) { + BuiltinScope scope = BuiltinScope.from(classInfo); + if (BuiltinScope.REQUEST != scope) { + throw new DeploymentException( + "Resource classes that use field injection for REST parameters can only be @RequestScoped. Offending class is " + + classInfo.name()); + } + } + } + }
extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveCDIProcessor.java+44 −0 modified@@ -11,7 +11,10 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.spi.DeploymentException; import jakarta.ws.rs.BeanParam; import org.jboss.jandex.AnnotationInstance; @@ -67,6 +70,47 @@ void beanDefiningAnnotations(BuildProducer<BeanDefiningAnnotationBuildItem> bean BuiltinScope.SINGLETON.getName())); } + /** + * The idea here is to make a best effort to find resources that need to be {@link RequestScoped} + * and make them such if no scope has been defined. + * If any other scope has been explicitly defined, the build will fail + */ + @BuildStep + void requestScopedResources(Optional<ResourceScanningResultBuildItem> resourceScanningResultBuildItem, + BuildProducer<AnnotationsTransformerBuildItem> additionalBeanBuildItemBuildProducer) { + if (resourceScanningResultBuildItem.isEmpty()) { + return; + } + Set<DotName> requestScopedResources = resourceScanningResultBuildItem.get().getResult() + .getRequestScopedResources(); + + additionalBeanBuildItemBuildProducer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + @Override + public boolean appliesTo(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.CLASS; + } + + @Override + public void transform(TransformationContext transformationContext) { + ClassInfo clazz = transformationContext.getTarget().asClass(); + if (requestScopedResources.contains(clazz.name())) { + BuiltinScope builtinScope = BuiltinScope.from(clazz); + if (builtinScope != null) { + if (builtinScope.getName() != BuiltinScope.REQUEST.getName()) { + throw new DeploymentException( + "Resource classes that use field injection for REST parameters can only be @RequestScoped. Offending class is " + + clazz.name()); + } else { + // nothing to do as @RequestScoped was already present + } + } else { + transformationContext.transform().add(RequestScoped.class).done(); + } + } + } + })); + } + @BuildStep void unremovableContextMethodParams(Optional<ResourceScanningResultBuildItem> resourceScanningResultBuildItem, BuildProducer<UnremovableBeanBuildItem> producer) {
extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java+2 −1 modified@@ -632,7 +632,8 @@ public Supplier<Boolean> apply(ClassInfo classInfo) { boolean disableIfMissing = disableIfMissingValue != null && disableIfMissingValue.asBoolean(); return recorder.disableIfPropertyMatches(propertyName, propertyValue, disableIfMissing); } - }); + }) + .alreadyHandledRequestScopedResources(result.getRequestScopedResources()); if (!serverDefaultProducesHandlers.isEmpty()) { List<DefaultProducesHandler> handlers = new ArrayList<>(serverDefaultProducesHandlers.size());
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/FormFieldSingletonScopeTest.java+44 −0 added@@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.inject.Singleton; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FormFieldSingletonScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)) + .assertException(t -> { + org.junit.jupiter.api.Assertions.assertEquals(DeploymentException.class, t.getClass()); + }); + + @Test + public void test() { + Assertions.fail("should never have run"); + } + + @Path("/test") + @Singleton + public static class Resource { + + @FormParam("foo") + String foo; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo; + } + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldInSuperClassDependentScopeTest.java+50 −0 added@@ -0,0 +1,50 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.assertj.core.api.Assertions; +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldInSuperClassDependentScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(AbstractResource.class, Resource.class)) + .assertException(t -> { + org.junit.jupiter.api.Assertions.assertEquals(DeploymentException.class, t.getClass()); + }); + + @Test + public void test() { + Assertions.fail("should never have run"); + } + + @Path("/test") + @Dependent + public static class Resource extends AbstractResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } + + public static class AbstractResource { + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldInSuperClassNoScopeTest.java+66 −0 added@@ -0,0 +1,66 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldInSuperClassNoScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(AbstractResource.class, AbstractAbstractResource.class, Resource.class)); + + @Test + public void test() { + given() + .header("foo", "f") + .header("bar", "b") + .when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + @RequestScoped + public static class Resource extends AbstractAbstractResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } + + public static class AbstractResource { + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + } + + public static class AbstractAbstractResource extends AbstractResource { + + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldInSuperClassRequestScopeTest.java+62 −0 added@@ -0,0 +1,62 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldInSuperClassRequestScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(AbstractResource.class, Resource.class)); + + @Test + public void test() { + given() + .header("foo", "f") + .header("bar", "b") + .when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + @RequestScoped + public static class Resource extends AbstractResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } + + public static class AbstractResource { + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldNoScopeTest.java+58 −0 added@@ -0,0 +1,58 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldNoScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)); + + @Test + public void test() { + given() + .header("foo", "f") + .header("bar", "b") + .when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + public static class Resource { + + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/PathFieldApplicationScopeTest.java+45 −0 added@@ -0,0 +1,45 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.assertj.core.api.Assertions; +import org.jboss.resteasy.reactive.RestPath; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class PathFieldApplicationScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)) + .assertException(t -> { + org.junit.jupiter.api.Assertions.assertEquals(DeploymentException.class, t.getClass()); + }); + + @Test + public void test() { + Assertions.fail("should never have run"); + } + + @Path("/test") + @Singleton + public static class Resource { + + @RestPath + String id; + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/{id}") + public String hello() { + return "id: " + id; + } + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/QueryFieldRequestScopeTest.java+56 −0 added@@ -0,0 +1,56 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestQuery; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class QueryFieldRequestScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)); + + @Test + public void test() { + when() + .get("/test?foo=f&bar=b") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + @RequestScoped + public static class Resource { + + @QueryParam("foo") + String foo; + + @RestQuery + String bar; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } +}
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java+18 −0 modified@@ -238,6 +238,7 @@ public abstract class EndpointIndexer<T extends EndpointIndexer<T, PARAM, METHOD private final Predicate<Map<DotName, AnnotationInstance>> skipMethodParameter; private SerializerScanningResult serializerScanningResult; + protected final Set<DotName> alreadyHandledRequestScopedResources; protected EndpointIndexer(Builder<T, ?, METHOD> builder) { this.index = builder.index; @@ -262,6 +263,7 @@ protected EndpointIndexer(Builder<T, ?, METHOD> builder) { this.targetJavaVersion = builder.targetJavaVersion; this.isDisabledCreator = builder.isDisabledCreator; this.skipMethodParameter = builder.skipMethodParameter; + this.alreadyHandledRequestScopedResources = builder.alreadyHandledRequestScopedResources; } public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean considerApplication) { @@ -310,6 +312,9 @@ public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean cons } } if (injectableBean.isInjectionRequired()) { + if (path != null) { // we don't want to verify subresources + verifyClassThatRequiresFieldInjection(classInfo); + } clazz.setPerRequestResource(true); } @@ -319,6 +324,9 @@ public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean cons return Optional.of(clazz); } catch (Exception e) { + if (e instanceof DeploymentException) { + throw (DeploymentException) e; + } if (Modifier.isInterface(classInfo.flags()) || Modifier.isAbstract(classInfo.flags())) { //kinda bogus, but we just ignore failed interfaces for now //they can have methods that are not valid until they are actually extended by a concrete type @@ -329,6 +337,10 @@ public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean cons } } + protected void verifyClassThatRequiresFieldInjection(ClassInfo classInfo) { + + } + private String sanitizePath(String path) { // this simply replaces the whitespace characters (not part of a path variable) with %20 // TODO: this might have to be more complex, URL encoding maybe? @@ -1664,6 +1676,7 @@ public static abstract class Builder<T extends EndpointIndexer<T, ?, METHOD>, B private Consumer<ResourceMethodCallbackEntry> resourceMethodCallback; private Collection<AnnotationsTransformer> annotationsTransformers; private ApplicationScanningResult applicationScanningResult; + private Set<DotName> alreadyHandledRequestScopedResources = new HashSet<>(); private final Set<DotName> contextTypes = new HashSet<>(DEFAULT_CONTEXT_TYPES); private final Set<DotName> parameterContainerTypes = new HashSet<>(); private MultipartReturnTypeIndexerExtension multipartReturnTypeIndexerExtension = new MultipartReturnTypeIndexerExtension() { @@ -1801,6 +1814,11 @@ public B setSkipMethodParameter( return (B) this; } + public B alreadyHandledRequestScopedResources(Set<DotName> alreadyHandledRequestScopedResources) { + this.alreadyHandledRequestScopedResources = alreadyHandledRequestScopedResources; + return (B) this; + } + public abstract T build(); }
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java+2 −0 modified@@ -26,6 +26,7 @@ import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.inject.Typed; import jakarta.enterprise.inject.Vetoed; @@ -175,6 +176,7 @@ public final class ResteasyReactiveDotNames { public static final DotName APPLICATION_SCOPED = DotName.createSimple(ApplicationScoped.class.getName()); public static final DotName SINGLETON = DotName.createSimple(Singleton.class.getName()); public static final DotName REQUEST_SCOPED = DotName.createSimple(RequestScoped.class.getName()); + public static final DotName DEPENDENT = DotName.createSimple(Dependent.class.getName()); public static final DotName WEB_APPLICATION_EXCEPTION = DotName.createSimple(WebApplicationException.class.getName()); public static final DotName INVOCATION_CALLBACK = DotName.createSimple(InvocationCallback.class.getName());
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResourceScanningResult.java+8 −1 modified@@ -21,13 +21,15 @@ public final class ResourceScanningResult { final Set<String> beanParams; final Map<DotName, String> httpAnnotationToMethod; final List<MethodInfo> classLevelExceptionMappers; + final Set<DotName> requestScopedResources; public ResourceScanningResult(IndexView index, Map<DotName, ClassInfo> scannedResources, Map<DotName, String> scannedResourcePaths, Map<DotName, ClassInfo> possibleSubResources, Map<DotName, String> pathInterfaces, Map<DotName, String> clientInterfaces, Map<DotName, MethodInfo> resourcesThatNeedCustomProducer, - Set<String> beanParams, Map<DotName, String> httpAnnotationToMethod, List<MethodInfo> classLevelExceptionMappers) { + Set<String> beanParams, Map<DotName, String> httpAnnotationToMethod, List<MethodInfo> classLevelExceptionMappers, + Set<DotName> requestScopedResources) { this.index = index; this.scannedResources = scannedResources; this.scannedResourcePaths = scannedResourcePaths; @@ -38,6 +40,7 @@ public ResourceScanningResult(IndexView index, Map<DotName, ClassInfo> scannedRe this.beanParams = beanParams; this.httpAnnotationToMethod = httpAnnotationToMethod; this.classLevelExceptionMappers = classLevelExceptionMappers; + this.requestScopedResources = requestScopedResources; } public IndexView getIndex() { @@ -79,4 +82,8 @@ public Map<DotName, String> getHttpAnnotationToMethod() { public List<MethodInfo> getClassLevelExceptionMappers() { return classLevelExceptionMappers; } + + public Set<DotName> getRequestScopedResources() { + return requestScopedResources; + } }
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java+44 −1 modified@@ -1,12 +1,25 @@ package org.jboss.resteasy.reactive.common.processor.scanning; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.BEAN_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COOKIE_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.DELETE; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.FORM_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.GET; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.HEAD; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.HEADER_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MATRIX_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OPTIONS; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PATCH; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PATH_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.POST; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PUT; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.QUERY_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_COOKIE_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_FORM_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_HEADER_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_MATRIX_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_PATH_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_QUERY_PARAM; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; @@ -228,6 +241,7 @@ public static ResourceScanningResult scanResources( Map<DotName, String> pathInterfaces = new HashMap<>(); Map<DotName, MethodInfo> resourcesThatNeedCustomProducer = new HashMap<>(); List<MethodInfo> methodExceptionMappers = new ArrayList<>(); + Set<DotName> requestScopedResources = new HashSet<>(); Set<DotName> interfacesWithPathOnMethods = new HashSet<>(); @@ -244,6 +258,9 @@ public static ResourceScanningResult scanResources( if (ctor != null) { resourcesThatNeedCustomProducer.put(clazz.name(), ctor); } + if (hasJaxRsFieldInjection(clazz, index)) { + requestScopedResources.add(clazz.name()); + } List<AnnotationInstance> exceptionMapperAnnotationInstances = clazz.annotationsMap() .get(ResteasyReactiveDotNames.SERVER_EXCEPTION_MAPPER); if (exceptionMapperAnnotationInstances != null) { @@ -430,7 +447,7 @@ public static ResourceScanningResult scanResources( return new ResourceScanningResult(index, scannedResources, scannedResourcePaths, possibleSubResources, pathInterfaces, clientInterfaces, resourcesThatNeedCustomProducer, beanParams, - httpAnnotationToMethod, methodExceptionMappers); + httpAnnotationToMethod, methodExceptionMappers, requestScopedResources); } private static void addClientSubInterfaces(DotName interfaceName, IndexView index, @@ -472,4 +489,30 @@ private static MethodInfo hasJaxRsCtorParams(ClassInfo classInfo) { return needsHandling ? ctor : null; } + public static final Set<DotName> ANNOTATIONS_REQUIRING_FIELD_INJECTION = new HashSet<>( + Arrays.asList(PATH_PARAM, QUERY_PARAM, HEADER_PARAM, FORM_PARAM, MATRIX_PARAM, + COOKIE_PARAM, REST_PATH_PARAM, REST_QUERY_PARAM, REST_HEADER_PARAM, REST_FORM_PARAM, REST_MATRIX_PARAM, + REST_COOKIE_PARAM, BEAN_PARAM)); + + private static boolean hasJaxRsFieldInjection(ClassInfo classInfo, IndexView index) { + while (true) { + for (FieldInfo field : classInfo.fields()) { + List<AnnotationInstance> annotations = field.annotations(); + if (annotations.stream() + .anyMatch(an -> ANNOTATIONS_REQUIRING_FIELD_INJECTION.contains(an.name()))) { + return true; + } + } + DotName parentDotName = classInfo.superName(); + if (parentDotName.equals(ResteasyReactiveDotNames.OBJECT)) { + return false; + } + classInfo = index.getClassByName(parentDotName); + if (classInfo == null) { + return false; + } + } + + } + }
f42166ee7041Merge pull request #46175 from geoand/3.15-#45789
14 files changed · +530 −3
extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusServerEndpointIndexer.java+23 −0 modified@@ -8,6 +8,8 @@ import java.util.Map; import java.util.function.Predicate; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.spi.DeploymentException; import jakarta.ws.rs.core.MediaType; import org.jboss.jandex.AnnotationInstance; @@ -28,6 +30,7 @@ import org.jboss.resteasy.reactive.server.processor.ServerIndexedParameter; import org.jboss.resteasy.reactive.server.spi.EndpointInvokerFactory; +import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.builder.BuildException; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; @@ -274,4 +277,24 @@ protected void warnAboutMissUsedBodyParameter(DotName httpMethod, MethodInfo met super.warnAboutMissUsedBodyParameter(httpMethod, methodInfo); } + /** + * At this point we know exactly which resources will require field injection and therefore are required to be + * {@link RequestScoped}. + * We can't change anything CDI related at this point (because it would create build cycles), so all we can do + * is fail the build if the resource has not already been handled automatically (by the best effort approach performed + * elsewhere) + * or it's not manually set to be {@link RequestScoped}. + */ + @Override + protected void verifyClassThatRequiresFieldInjection(ClassInfo classInfo) { + if (!alreadyHandledRequestScopedResources.contains(classInfo.name())) { + BuiltinScope scope = BuiltinScope.from(classInfo); + if (BuiltinScope.REQUEST != scope) { + throw new DeploymentException( + "Resource classes that use field injection for REST parameters can only be @RequestScoped. Offending class is " + + classInfo.name()); + } + } + } + }
extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveCDIProcessor.java+52 −0 modified@@ -11,12 +11,19 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.spi.DeploymentException; import jakarta.ws.rs.BeanParam; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.Declaration; import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; @@ -67,6 +74,51 @@ void beanDefiningAnnotations(BuildProducer<BeanDefiningAnnotationBuildItem> bean BuiltinScope.SINGLETON.getName())); } + /** + * The idea here is to make a best effort to find resources that need to be {@link RequestScoped} + * and make them such if no scope has been defined. + * If any other scope has been explicitly defined, the build will fail + */ + @BuildStep + void requestScopedResources(Optional<ResourceScanningResultBuildItem> resourceScanningResultBuildItem, + BuildProducer<AnnotationsTransformerBuildItem> additionalBeanBuildItemBuildProducer) { + if (resourceScanningResultBuildItem.isEmpty()) { + return; + } + Set<DotName> requestScopedResources = resourceScanningResultBuildItem.get().getResult() + .getRequestScopedResources(); + + additionalBeanBuildItemBuildProducer.produce(new io.quarkus.arc.deployment.AnnotationsTransformerBuildItem( + AnnotationTransformation.builder().whenDeclaration( + new Predicate<>() { + @Override + public boolean test(Declaration declaration) { + return declaration.kind() == AnnotationTarget.Kind.CLASS; + } + }).transform(new Consumer<>() { + @Override + public void accept(AnnotationTransformation.TransformationContext context) { + if (context.declaration().kind() == AnnotationTarget.Kind.CLASS) { + ClassInfo clazz = context.declaration().asClass(); + if (requestScopedResources.contains(clazz.name())) { + BuiltinScope builtinScope = BuiltinScope.from(clazz); + if (builtinScope != null) { + if (builtinScope.getName() != BuiltinScope.REQUEST.getName()) { + throw new DeploymentException( + "Resource classes that use field injection for REST parameters can only be @RequestScoped. Offending class is " + + clazz.name()); + } else { + // nothing to do as @RequestScoped was already present + } + } else { + context.add(RequestScoped.class); + } + } + } + } + }))); + } + @BuildStep void unremovableContextMethodParams(Optional<ResourceScanningResultBuildItem> resourceScanningResultBuildItem, BuildProducer<UnremovableBeanBuildItem> producer) {
extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java+2 −1 modified@@ -627,7 +627,8 @@ public Supplier<Boolean> apply(ClassInfo classInfo) { boolean disableIfMissing = disableIfMissingValue != null && disableIfMissingValue.asBoolean(); return recorder.disableIfPropertyMatches(propertyName, propertyValue, disableIfMissing); } - }); + }) + .alreadyHandledRequestScopedResources(result.getRequestScopedResources()); serverEndpointIndexerBuilder.skipNotRestParameters(allowNotRestParametersBuildItem.isPresent());
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/FormFieldSingletonScopeTest.java+44 −0 added@@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.inject.Singleton; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FormFieldSingletonScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)) + .assertException(t -> { + org.junit.jupiter.api.Assertions.assertEquals(DeploymentException.class, t.getClass()); + }); + + @Test + public void test() { + Assertions.fail("should never have run"); + } + + @Path("/test") + @Singleton + public static class Resource { + + @FormParam("foo") + String foo; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo; + } + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldInSuperClassDependentScopeTest.java+50 −0 added@@ -0,0 +1,50 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.assertj.core.api.Assertions; +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldInSuperClassDependentScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(AbstractResource.class, Resource.class)) + .assertException(t -> { + org.junit.jupiter.api.Assertions.assertEquals(DeploymentException.class, t.getClass()); + }); + + @Test + public void test() { + Assertions.fail("should never have run"); + } + + @Path("/test") + @Dependent + public static class Resource extends AbstractResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } + + public static class AbstractResource { + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldInSuperClassNoScopeTest.java+66 −0 added@@ -0,0 +1,66 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldInSuperClassNoScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(AbstractResource.class, AbstractAbstractResource.class, Resource.class)); + + @Test + public void test() { + given() + .header("foo", "f") + .header("bar", "b") + .when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + @RequestScoped + public static class Resource extends AbstractAbstractResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } + + public static class AbstractResource { + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + } + + public static class AbstractAbstractResource extends AbstractResource { + + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldInSuperClassRequestScopeTest.java+62 −0 added@@ -0,0 +1,62 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldInSuperClassRequestScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(AbstractResource.class, Resource.class)); + + @Test + public void test() { + given() + .header("foo", "f") + .header("bar", "b") + .when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + @RequestScoped + public static class Resource extends AbstractResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } + + public static class AbstractResource { + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldNoScopeTest.java+58 −0 added@@ -0,0 +1,58 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldNoScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)); + + @Test + public void test() { + given() + .header("foo", "f") + .header("bar", "b") + .when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + public static class Resource { + + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/PathFieldApplicationScopeTest.java+45 −0 added@@ -0,0 +1,45 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.assertj.core.api.Assertions; +import org.jboss.resteasy.reactive.RestPath; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class PathFieldApplicationScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)) + .assertException(t -> { + org.junit.jupiter.api.Assertions.assertEquals(DeploymentException.class, t.getClass()); + }); + + @Test + public void test() { + Assertions.fail("should never have run"); + } + + @Path("/test") + @Singleton + public static class Resource { + + @RestPath + String id; + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/{id}") + public String hello() { + return "id: " + id; + } + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/QueryFieldRequestScopeTest.java+56 −0 added@@ -0,0 +1,56 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestQuery; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class QueryFieldRequestScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)); + + @Test + public void test() { + when() + .get("/test?foo=f&bar=b") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + @RequestScoped + public static class Resource { + + @QueryParam("foo") + String foo; + + @RestQuery + String bar; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } +}
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java+18 −0 modified@@ -240,6 +240,7 @@ public abstract class EndpointIndexer<T extends EndpointIndexer<T, PARAM, METHOD private final Predicate<Map<DotName, AnnotationInstance>> skipMethodParameter; private final boolean skipNotRestParameters; + protected final Set<DotName> alreadyHandledRequestScopedResources; private SerializerScanningResult serializerScanningResult; @@ -267,6 +268,7 @@ protected EndpointIndexer(Builder<T, ?, METHOD> builder) { this.isDisabledCreator = builder.isDisabledCreator; this.skipMethodParameter = builder.skipMethodParameter; this.skipNotRestParameters = builder.skipNotRestParameters; + this.alreadyHandledRequestScopedResources = builder.alreadyHandledRequestScopedResources; } public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean considerApplication) { @@ -315,6 +317,9 @@ public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean cons } } if (injectableBean.isInjectionRequired()) { + if (path != null) { // we don't want to verify subresources + verifyClassThatRequiresFieldInjection(classInfo); + } clazz.setPerRequestResource(true); } @@ -324,6 +329,9 @@ public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean cons return Optional.of(clazz); } catch (Exception e) { + if (e instanceof DeploymentException) { + throw (DeploymentException) e; + } if (Modifier.isInterface(classInfo.flags()) || Modifier.isAbstract(classInfo.flags())) { //kinda bogus, but we just ignore failed interfaces for now //they can have methods that are not valid until they are actually extended by a concrete type @@ -334,6 +342,10 @@ public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean cons } } + protected void verifyClassThatRequiresFieldInjection(ClassInfo classInfo) { + + } + private String sanitizePath(String path) { // this simply replaces the whitespace characters (not part of a path variable) with %20 // TODO: this might have to be more complex, URL encoding maybe? @@ -1682,6 +1694,7 @@ public static abstract class Builder<T extends EndpointIndexer<T, ?, METHOD>, B private Consumer<ResourceMethodCallbackEntry> resourceMethodCallback; private Collection<AnnotationTransformation> annotationsTransformers; private ApplicationScanningResult applicationScanningResult; + private Set<DotName> alreadyHandledRequestScopedResources = new HashSet<>(); private final Set<DotName> contextTypes = new HashSet<>(DEFAULT_CONTEXT_TYPES); private final Set<DotName> parameterContainerTypes = new HashSet<>(); private MultipartReturnTypeIndexerExtension multipartReturnTypeIndexerExtension = new MultipartReturnTypeIndexerExtension() { @@ -1837,6 +1850,11 @@ public B skipNotRestParameters(boolean skipNotRestParameters) { return (B) this; } + public B alreadyHandledRequestScopedResources(Set<DotName> alreadyHandledRequestScopedResources) { + this.alreadyHandledRequestScopedResources = alreadyHandledRequestScopedResources; + return (B) this; + } + public abstract T build(); }
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java+2 −0 modified@@ -26,6 +26,7 @@ import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.inject.Typed; import jakarta.enterprise.inject.Vetoed; @@ -175,6 +176,7 @@ public final class ResteasyReactiveDotNames { public static final DotName APPLICATION_SCOPED = DotName.createSimple(ApplicationScoped.class.getName()); public static final DotName SINGLETON = DotName.createSimple(Singleton.class.getName()); public static final DotName REQUEST_SCOPED = DotName.createSimple(RequestScoped.class.getName()); + public static final DotName DEPENDENT = DotName.createSimple(Dependent.class.getName()); public static final DotName WEB_APPLICATION_EXCEPTION = DotName.createSimple(WebApplicationException.class.getName()); public static final DotName INVOCATION_CALLBACK = DotName.createSimple(InvocationCallback.class.getName());
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResourceScanningResult.java+8 −1 modified@@ -21,13 +21,15 @@ public final class ResourceScanningResult { final Set<String> beanParams; final Map<DotName, String> httpAnnotationToMethod; final List<MethodInfo> classLevelExceptionMappers; + final Set<DotName> requestScopedResources; public ResourceScanningResult(IndexView index, Map<DotName, ClassInfo> scannedResources, Map<DotName, String> scannedResourcePaths, Map<DotName, ClassInfo> possibleSubResources, Map<DotName, String> pathInterfaces, Map<DotName, String> clientInterfaces, Map<DotName, MethodInfo> resourcesThatNeedCustomProducer, - Set<String> beanParams, Map<DotName, String> httpAnnotationToMethod, List<MethodInfo> classLevelExceptionMappers) { + Set<String> beanParams, Map<DotName, String> httpAnnotationToMethod, List<MethodInfo> classLevelExceptionMappers, + Set<DotName> requestScopedResources) { this.index = index; this.scannedResources = scannedResources; this.scannedResourcePaths = scannedResourcePaths; @@ -38,6 +40,7 @@ public ResourceScanningResult(IndexView index, Map<DotName, ClassInfo> scannedRe this.beanParams = beanParams; this.httpAnnotationToMethod = httpAnnotationToMethod; this.classLevelExceptionMappers = classLevelExceptionMappers; + this.requestScopedResources = requestScopedResources; } public IndexView getIndex() { @@ -79,4 +82,8 @@ public Map<DotName, String> getHttpAnnotationToMethod() { public List<MethodInfo> getClassLevelExceptionMappers() { return classLevelExceptionMappers; } + + public Set<DotName> getRequestScopedResources() { + return requestScopedResources; + } }
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java+44 −1 modified@@ -1,12 +1,25 @@ package org.jboss.resteasy.reactive.common.processor.scanning; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.BEAN_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COOKIE_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.DELETE; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.FORM_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.GET; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.HEAD; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.HEADER_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MATRIX_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OPTIONS; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PATCH; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PATH_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.POST; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PUT; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.QUERY_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_COOKIE_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_FORM_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_HEADER_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_MATRIX_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_PATH_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_QUERY_PARAM; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; @@ -219,6 +232,7 @@ public static ResourceScanningResult scanResources( Map<DotName, String> pathInterfaces = new HashMap<>(); Map<DotName, MethodInfo> resourcesThatNeedCustomProducer = new HashMap<>(); List<MethodInfo> methodExceptionMappers = new ArrayList<>(); + Set<DotName> requestScopedResources = new HashSet<>(); Set<DotName> interfacesWithPathOnMethods = new HashSet<>(); @@ -235,6 +249,9 @@ public static ResourceScanningResult scanResources( if (ctor != null) { resourcesThatNeedCustomProducer.put(clazz.name(), ctor); } + if (hasJaxRsFieldInjection(clazz, index)) { + requestScopedResources.add(clazz.name()); + } List<AnnotationInstance> exceptionMapperAnnotationInstances = clazz.annotationsMap() .get(ResteasyReactiveDotNames.SERVER_EXCEPTION_MAPPER); if (exceptionMapperAnnotationInstances != null) { @@ -421,7 +438,7 @@ public static ResourceScanningResult scanResources( return new ResourceScanningResult(index, scannedResources, scannedResourcePaths, possibleSubResources, pathInterfaces, clientInterfaces, resourcesThatNeedCustomProducer, beanParams, - httpAnnotationToMethod, methodExceptionMappers); + httpAnnotationToMethod, methodExceptionMappers, requestScopedResources); } private static void addClientSubInterfaces(DotName interfaceName, IndexView index, @@ -463,4 +480,30 @@ private static MethodInfo hasJaxRsCtorParams(ClassInfo classInfo) { return needsHandling ? ctor : null; } + public static final Set<DotName> ANNOTATIONS_REQUIRING_FIELD_INJECTION = new HashSet<>( + Arrays.asList(PATH_PARAM, QUERY_PARAM, HEADER_PARAM, FORM_PARAM, MATRIX_PARAM, + COOKIE_PARAM, REST_PATH_PARAM, REST_QUERY_PARAM, REST_HEADER_PARAM, REST_FORM_PARAM, REST_MATRIX_PARAM, + REST_COOKIE_PARAM, BEAN_PARAM)); + + private static boolean hasJaxRsFieldInjection(ClassInfo classInfo, IndexView index) { + while (true) { + for (FieldInfo field : classInfo.fields()) { + List<AnnotationInstance> annotations = field.annotations(); + if (annotations.stream() + .anyMatch(an -> ANNOTATIONS_REQUIRING_FIELD_INJECTION.contains(an.name()))) { + return true; + } + } + DotName parentDotName = classInfo.superName(); + if (parentDotName.equals(ResteasyReactiveDotNames.OBJECT)) { + return false; + } + classInfo = index.getClassByName(parentDotName); + if (classInfo == null) { + return false; + } + } + + } + }
02ff9ed45c39Merge pull request #45950 from geoand/#45789
14 files changed · +532 −3
extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusServerEndpointIndexer.java+23 −0 modified@@ -8,6 +8,8 @@ import java.util.Map; import java.util.function.Predicate; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.spi.DeploymentException; import jakarta.ws.rs.core.MediaType; import org.jboss.jandex.AnnotationInstance; @@ -28,6 +30,7 @@ import org.jboss.resteasy.reactive.server.processor.ServerIndexedParameter; import org.jboss.resteasy.reactive.server.spi.EndpointInvokerFactory; +import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.builder.BuildException; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; @@ -274,4 +277,24 @@ protected void warnAboutMissUsedBodyParameter(DotName httpMethod, MethodInfo met super.warnAboutMissUsedBodyParameter(httpMethod, methodInfo); } + /** + * At this point we know exactly which resources will require field injection and therefore are required to be + * {@link RequestScoped}. + * We can't change anything CDI related at this point (because it would create build cycles), so all we can do + * is fail the build if the resource has not already been handled automatically (by the best effort approach performed + * elsewhere) + * or it's not manually set to be {@link RequestScoped}. + */ + @Override + protected void verifyClassThatRequiresFieldInjection(ClassInfo classInfo) { + if (!alreadyHandledRequestScopedResources.contains(classInfo.name())) { + BuiltinScope scope = BuiltinScope.from(classInfo); + if (BuiltinScope.REQUEST != scope) { + throw new DeploymentException( + "Resource classes that use field injection for REST parameters can only be @RequestScoped. Offending class is " + + classInfo.name()); + } + } + } + }
extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveCDIProcessor.java+52 −0 modified@@ -11,12 +11,19 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.spi.DeploymentException; import jakarta.ws.rs.BeanParam; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.Declaration; import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; @@ -67,6 +74,51 @@ void beanDefiningAnnotations(BuildProducer<BeanDefiningAnnotationBuildItem> bean BuiltinScope.SINGLETON.getName())); } + /** + * The idea here is to make a best effort to find resources that need to be {@link RequestScoped} + * and make them such if no scope has been defined. + * If any other scope has been explicitly defined, the build will fail + */ + @BuildStep + void requestScopedResources(Optional<ResourceScanningResultBuildItem> resourceScanningResultBuildItem, + BuildProducer<AnnotationsTransformerBuildItem> additionalBeanBuildItemBuildProducer) { + if (resourceScanningResultBuildItem.isEmpty()) { + return; + } + Set<DotName> requestScopedResources = resourceScanningResultBuildItem.get().getResult() + .getRequestScopedResources(); + + additionalBeanBuildItemBuildProducer.produce(new io.quarkus.arc.deployment.AnnotationsTransformerBuildItem( + AnnotationTransformation.builder().whenDeclaration( + new Predicate<>() { + @Override + public boolean test(Declaration declaration) { + return declaration.kind() == AnnotationTarget.Kind.CLASS; + } + }).transform(new Consumer<>() { + @Override + public void accept(AnnotationTransformation.TransformationContext context) { + if (context.declaration().kind() == AnnotationTarget.Kind.CLASS) { + ClassInfo clazz = context.declaration().asClass(); + if (requestScopedResources.contains(clazz.name())) { + BuiltinScope builtinScope = BuiltinScope.from(clazz); + if (builtinScope != null) { + if (builtinScope.getName() != BuiltinScope.REQUEST.getName()) { + throw new DeploymentException( + "Resource classes that use field injection for REST parameters can only be @RequestScoped. Offending class is " + + clazz.name()); + } else { + // nothing to do as @RequestScoped was already present + } + } else { + context.add(RequestScoped.class); + } + } + } + } + }))); + } + @BuildStep void unremovableContextMethodParams(Optional<ResourceScanningResultBuildItem> resourceScanningResultBuildItem, BuildProducer<UnremovableBeanBuildItem> producer) {
extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java+2 −1 modified@@ -682,7 +682,8 @@ public Supplier<Boolean> apply(ClassInfo classInfo) { boolean disableIfMissing = disableIfMissingValue != null && disableIfMissingValue.asBoolean(); return recorder.disableIfPropertyMatches(propertyName, propertyValue, disableIfMissing); } - }); + }) + .alreadyHandledRequestScopedResources(result.getRequestScopedResources()); serverEndpointIndexerBuilder.skipNotRestParameters(allowNotRestParametersBuildItem.isPresent());
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/FormFieldSingletonScopeTest.java+44 −0 added@@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.inject.Singleton; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FormFieldSingletonScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)) + .assertException(t -> { + org.junit.jupiter.api.Assertions.assertEquals(DeploymentException.class, t.getClass()); + }); + + @Test + public void test() { + Assertions.fail("should never have run"); + } + + @Path("/test") + @Singleton + public static class Resource { + + @FormParam("foo") + String foo; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo; + } + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldInSuperClassDependentScopeTest.java+50 −0 added@@ -0,0 +1,50 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.assertj.core.api.Assertions; +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldInSuperClassDependentScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(AbstractResource.class, Resource.class)) + .assertException(t -> { + org.junit.jupiter.api.Assertions.assertEquals(DeploymentException.class, t.getClass()); + }); + + @Test + public void test() { + Assertions.fail("should never have run"); + } + + @Path("/test") + @Dependent + public static class Resource extends AbstractResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } + + public static class AbstractResource { + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldInSuperClassNoScopeTest.java+66 −0 added@@ -0,0 +1,66 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldInSuperClassNoScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(AbstractResource.class, AbstractAbstractResource.class, Resource.class)); + + @Test + public void test() { + given() + .header("foo", "f") + .header("bar", "b") + .when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + @RequestScoped + public static class Resource extends AbstractAbstractResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } + + public static class AbstractResource { + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + } + + public static class AbstractAbstractResource extends AbstractResource { + + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldInSuperClassRequestScopeTest.java+62 −0 added@@ -0,0 +1,62 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldInSuperClassRequestScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(AbstractResource.class, Resource.class)); + + @Test + public void test() { + given() + .header("foo", "f") + .header("bar", "b") + .when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + @RequestScoped + public static class Resource extends AbstractResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } + + public static class AbstractResource { + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/HeaderFieldNoScopeTest.java+58 −0 added@@ -0,0 +1,58 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class HeaderFieldNoScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)); + + @Test + public void test() { + given() + .header("foo", "f") + .header("bar", "b") + .when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + public static class Resource { + + @HeaderParam("foo") + String foo; + + @RestHeader + String bar; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/PathFieldApplicationScopeTest.java+45 −0 added@@ -0,0 +1,45 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.assertj.core.api.Assertions; +import org.jboss.resteasy.reactive.RestPath; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class PathFieldApplicationScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)) + .assertException(t -> { + org.junit.jupiter.api.Assertions.assertEquals(DeploymentException.class, t.getClass()); + }); + + @Test + public void test() { + Assertions.fail("should never have run"); + } + + @Path("/test") + @Singleton + public static class Resource { + + @RestPath + String id; + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/{id}") + public String hello() { + return "id: " + id; + } + } +}
extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/injection/QueryFieldRequestScopeTest.java+56 −0 added@@ -0,0 +1,56 @@ +package io.quarkus.resteasy.reactive.server.test.injection; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestQuery; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class QueryFieldRequestScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class)); + + @Test + public void test() { + when() + .get("/test?foo=f&bar=b") + .then() + .statusCode(200) + .body(is("foo: f, bar: b")); + + when() + .get("/test") + .then() + .statusCode(200) + .body(is("foo: null, bar: null")); + } + + @Path("/test") + @RequestScoped + public static class Resource { + + @QueryParam("foo") + String foo; + + @RestQuery + String bar; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo: " + foo + ", bar: " + bar; + } + } +}
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java+18 −0 modified@@ -243,6 +243,7 @@ public abstract class EndpointIndexer<T extends EndpointIndexer<T, PARAM, METHOD private final Predicate<Map<DotName, AnnotationInstance>> skipMethodParameter; private final List<Predicate<ClassInfo>> validateEndpoint; private final boolean skipNotRestParameters; + protected final Set<DotName> alreadyHandledRequestScopedResources; private SerializerScanningResult serializerScanningResult; @@ -271,6 +272,7 @@ protected EndpointIndexer(Builder<T, ?, METHOD> builder) { this.skipMethodParameter = builder.skipMethodParameter; this.skipNotRestParameters = builder.skipNotRestParameters; this.validateEndpoint = builder.defaultPredicate; + this.alreadyHandledRequestScopedResources = builder.alreadyHandledRequestScopedResources; } public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean considerApplication) { @@ -319,6 +321,9 @@ public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean cons } } if (injectableBean.isInjectionRequired()) { + if (path != null) { // we don't want to verify subresources + verifyClassThatRequiresFieldInjection(classInfo); + } clazz.setPerRequestResource(true); } @@ -328,6 +333,9 @@ public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean cons return Optional.of(clazz); } catch (Exception e) { + if (e instanceof DeploymentException) { + throw (DeploymentException) e; + } for (Predicate<ClassInfo> predicate : validateEndpoint) { if (predicate.test(classInfo)) { //kinda bogus, but we just ignore failed interfaces for now @@ -342,6 +350,10 @@ public Optional<ResourceClass> createEndpoints(ClassInfo classInfo, boolean cons } + protected void verifyClassThatRequiresFieldInjection(ClassInfo classInfo) { + + } + private String sanitizePath(String path) { // this simply replaces the whitespace characters (not part of a path variable) with %20 // TODO: this might have to be more complex, URL encoding maybe? @@ -1692,6 +1704,7 @@ public static abstract class Builder<T extends EndpointIndexer<T, ?, METHOD>, B private Consumer<ResourceMethodCallbackEntry> resourceMethodCallback; private Collection<AnnotationTransformation> annotationsTransformers; private ApplicationScanningResult applicationScanningResult; + private Set<DotName> alreadyHandledRequestScopedResources = new HashSet<>(); private final Set<DotName> contextTypes = new HashSet<>(DEFAULT_CONTEXT_TYPES); private final Set<DotName> parameterContainerTypes = new HashSet<>(); private MultipartReturnTypeIndexerExtension multipartReturnTypeIndexerExtension = new MultipartReturnTypeIndexerExtension() { @@ -1862,6 +1875,11 @@ public B skipNotRestParameters(boolean skipNotRestParameters) { return (B) this; } + public B alreadyHandledRequestScopedResources(Set<DotName> alreadyHandledRequestScopedResources) { + this.alreadyHandledRequestScopedResources = alreadyHandledRequestScopedResources; + return (B) this; + } + public abstract T build(); }
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java+2 −0 modified@@ -28,6 +28,7 @@ import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.inject.Typed; import jakarta.enterprise.inject.Vetoed; @@ -177,6 +178,7 @@ public final class ResteasyReactiveDotNames { public static final DotName APPLICATION_SCOPED = DotName.createSimple(ApplicationScoped.class.getName()); public static final DotName SINGLETON = DotName.createSimple(Singleton.class.getName()); public static final DotName REQUEST_SCOPED = DotName.createSimple(RequestScoped.class.getName()); + public static final DotName DEPENDENT = DotName.createSimple(Dependent.class.getName()); public static final DotName WEB_APPLICATION_EXCEPTION = DotName.createSimple(WebApplicationException.class.getName()); public static final DotName INVOCATION_CALLBACK = DotName.createSimple(InvocationCallback.class.getName());
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResourceScanningResult.java+9 −1 modified@@ -2,6 +2,7 @@ import java.util.List; import java.util.Map; +import java.util.Set; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -19,13 +20,15 @@ public final class ResourceScanningResult { final Map<DotName, MethodInfo> resourcesThatNeedCustomProducer; final Map<DotName, String> httpAnnotationToMethod; final List<MethodInfo> classLevelExceptionMappers; + final Set<DotName> requestScopedResources; public ResourceScanningResult(IndexView index, Map<DotName, ClassInfo> scannedResources, Map<DotName, String> scannedResourcePaths, Map<DotName, ClassInfo> possibleSubResources, Map<DotName, String> pathInterfaces, Map<DotName, String> clientInterfaces, Map<DotName, MethodInfo> resourcesThatNeedCustomProducer, - Map<DotName, String> httpAnnotationToMethod, List<MethodInfo> classLevelExceptionMappers) { + Map<DotName, String> httpAnnotationToMethod, List<MethodInfo> classLevelExceptionMappers, + Set<DotName> requestScopedResources) { this.index = index; this.scannedResources = scannedResources; this.scannedResourcePaths = scannedResourcePaths; @@ -35,6 +38,7 @@ public ResourceScanningResult(IndexView index, Map<DotName, ClassInfo> scannedRe this.resourcesThatNeedCustomProducer = resourcesThatNeedCustomProducer; this.httpAnnotationToMethod = httpAnnotationToMethod; this.classLevelExceptionMappers = classLevelExceptionMappers; + this.requestScopedResources = requestScopedResources; } public IndexView getIndex() { @@ -72,4 +76,8 @@ public Map<DotName, String> getHttpAnnotationToMethod() { public List<MethodInfo> getClassLevelExceptionMappers() { return classLevelExceptionMappers; } + + public Set<DotName> getRequestScopedResources() { + return requestScopedResources; + } }
independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java+45 −1 modified@@ -1,12 +1,25 @@ package org.jboss.resteasy.reactive.common.processor.scanning; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.BEAN_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COOKIE_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.DELETE; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.FORM_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.GET; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.HEAD; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.HEADER_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MATRIX_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OPTIONS; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PATCH; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PATH_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.POST; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PUT; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.QUERY_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_COOKIE_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_FORM_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_HEADER_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_MATRIX_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_PATH_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_QUERY_PARAM; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; @@ -32,6 +45,7 @@ import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; @@ -226,6 +240,7 @@ public static ResourceScanningResult scanResources( Map<DotName, String> pathInterfaces = new HashMap<>(); Map<DotName, MethodInfo> resourcesThatNeedCustomProducer = new HashMap<>(); List<MethodInfo> methodExceptionMappers = new ArrayList<>(); + Set<DotName> requestScopedResources = new HashSet<>(); Set<DotName> interfacesWithPathOnMethods = new HashSet<>(); @@ -242,6 +257,9 @@ public static ResourceScanningResult scanResources( if (ctor != null) { resourcesThatNeedCustomProducer.put(clazz.name(), ctor); } + if (hasJaxRsFieldInjection(clazz, index)) { + requestScopedResources.add(clazz.name()); + } List<AnnotationInstance> exceptionMapperAnnotationInstances = clazz.annotationsMap() .get(ResteasyReactiveDotNames.SERVER_EXCEPTION_MAPPER); if (exceptionMapperAnnotationInstances != null) { @@ -377,7 +395,7 @@ public static ResourceScanningResult scanResources( return new ResourceScanningResult(index, scannedResources, scannedResourcePaths, possibleSubResources, pathInterfaces, clientInterfaces, resourcesThatNeedCustomProducer, - httpAnnotationToMethod, methodExceptionMappers); + httpAnnotationToMethod, methodExceptionMappers, requestScopedResources); } private static void addClientSubInterfaces(DotName interfaceName, IndexView index, @@ -419,4 +437,30 @@ private static MethodInfo hasJaxRsCtorParams(ClassInfo classInfo) { return needsHandling ? ctor : null; } + public static final Set<DotName> ANNOTATIONS_REQUIRING_FIELD_INJECTION = new HashSet<>( + Arrays.asList(PATH_PARAM, QUERY_PARAM, HEADER_PARAM, FORM_PARAM, MATRIX_PARAM, + COOKIE_PARAM, REST_PATH_PARAM, REST_QUERY_PARAM, REST_HEADER_PARAM, REST_FORM_PARAM, REST_MATRIX_PARAM, + REST_COOKIE_PARAM, BEAN_PARAM)); + + private static boolean hasJaxRsFieldInjection(ClassInfo classInfo, IndexView index) { + while (true) { + for (FieldInfo field : classInfo.fields()) { + List<AnnotationInstance> annotations = field.annotations(); + if (annotations.stream() + .anyMatch(an -> ANNOTATIONS_REQUIRING_FIELD_INJECTION.contains(an.name()))) { + return true; + } + } + DotName parentDotName = classInfo.superName(); + if (parentDotName.equals(ResteasyReactiveDotNames.OBJECT)) { + return false; + } + classInfo = index.getClassByName(parentDotName); + if (classInfo == null) { + return false; + } + } + + } + }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
12- github.com/advisories/GHSA-phg3-gv66-q38xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-1247ghsaADVISORY
- access.redhat.com/errata/RHSA-2025:1884nvdWEB
- access.redhat.com/errata/RHSA-2025:1885nvdWEB
- access.redhat.com/errata/RHSA-2025:2067nvdWEB
- access.redhat.com/security/cve/CVE-2025-1247nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/quarkusio/quarkus/commit/02ff9ed45c3928edf2a0f8b906543606fed7cd53ghsaWEB
- github.com/quarkusio/quarkus/commit/d8df15cec17dc5d085efc372d77cbef1341ae071ghsaWEB
- github.com/quarkusio/quarkus/commit/f42166ee7041ed09b7183d5dbf3ece2439b16676ghsaWEB
- github.com/quarkusio/quarkus/issues/45789nvdWEB
- quarkus.io/blog/cve-fixes-feb-2025ghsaWEB
News mentions
0No linked articles in our index yet.