javaquarkus

Quarkus 3.21.4 -> 3.24.4 issues: "RequestScoped context was not active when trying to obtain a bean instance for a client proxy of CLASS bean"


After updating Quarkus in our project, from 3.21.4 to 3.24.4 our test suite now fails multiple tests (doesn't seem to always be the same amount, so there appears to be some flakiness) with this exception:

2025-09-01 10:20:30.558 ERROR traceId=ab33746340bdd8c93728c3403f544134, parentId=, spanId=ba3d07db6204374c, sampled=true, requesterId=dead1234cafe123f2812f93f <USER> co.ma.us.ap.RestExceptionMapper:174 [vert.x-eventloop-thread-4-552] An exception occurred: RequestScoped context was not active when trying to obtain a bean instance for a client proxy of CLASS bean [class=io.quarkus.vertx.http.runtime.CurrentVertxRequest, id=0_6n6EmChCiiDdd8HelptG_A0AE]
    - you can activate the request context for a specific method using the @ActivateRequestContext interceptor binding: jakarta.enterprise.context.ContextNotActiveException: RequestScoped context was not active when trying to obtain a bean instance for a client proxy of CLASS bean [class=io.quarkus.vertx.http.runtime.CurrentVertxRequest, id=0_6n6EmChCiiDdd8HelptG_A0AE]
    - you can activate the request context for a specific method using the @ActivateRequestContext interceptor binding
    at io.quarkus.arc.impl.ClientProxies.notActive(ClientProxies.java:76)
    at io.quarkus.arc.impl.ClientProxies.getSingleContextDelegate(ClientProxies.java:32)
    at io.quarkus.vertx.http.runtime.CurrentVertxRequest_ClientProxy.arc$delegate(Unknown Source)
    at io.quarkus.vertx.http.runtime.CurrentVertxRequest_ClientProxy.setCurrent(Unknown Source)
    at io.quarkus.resteasy.reactive.server.runtime.QuarkusCurrentRequest.set(QuarkusCurrentRequest.java:33)
    at org.jboss.resteasy.reactive.server.core.CurrentRequestManager.set(CurrentRequestManager.java:12)
    at org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext.handleRequestScopeActivation(ResteasyReactiveRequestContext.java:704)
    at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.handleRequestScopeActivation(QuarkusResteasyReactiveRequestContext.java:39)

I have tried using Cursor, CoPilot, even Junie (because we use IntelliJ in our project) to figure out a solution and they have been unable to figure it out, some of the things they tried:

Right now we're doing smaller bumps, we moved from 3.21.4 to 3.22.1 and everything was fine. Then we moved to 3.23.4 and the issues shows up. I have checked the 3.23 migration guide and I can't see anything that at least from my understanding would cause this.

I was expecting my project to run without issues when updating a minor version.

This is for the RestExceptionMapper:

public class RestExceptionMapper {

    private static final Logger LOG = Logger.getLogger(RestExceptionMapper.class);
    private static final String LOG_PREFIX = "An exception occurred: %s";

    private static final Set<Class<? extends Exception>> KNOWN_BAD_REQUEST_TYPES = Set.of(
        BadRequestException.class, JsonMappingException.class, JsonParseException.class, InvalidFormatException.class);

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(NotAllowedException e) {
        return createResponse(Status.METHOD_NOT_ALLOWED, e, null, USER_NOT_ALLOWED.getErrorCode());
    }

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(MismatchedInputException e) {
        return createResponse(BAD_REQUEST, null, REQUEST_BODY_IS_WRONGLY_FORMATTED);
    }

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(JsonProcessingException e) {
        return createResponse(BAD_REQUEST, null, REQUEST_BODY_IS_WRONGLY_FORMATTED);
    }

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(ResteasyReactiveViolationException e) {
        if (!e.getConstraintViolations()
            .stream()
            .filter(constraintViolation -> constraintViolation
                .getConstraintDescriptor()
                .getAnnotation()
                .annotationType()
                .getTypeName()
                .equals(ValidScopeConstraint.class.getTypeName()))
            .toList().isEmpty()) {
            return createResponse(UNAUTHORIZED, null, GENERIC_UNAUTHORIZED);
        }

        return createResponse(BAD_REQUEST, INVALID_REQUEST_FIELDS.getMessage(e.getMessage()), INVALID_REQUEST_FIELDS.getErrorCode());
    }

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(BadRequestException e) {
        return createResponse(BAD_REQUEST, e, null, GENERIC_BAD_REQUEST.getErrorCode());
    }

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(WebApplicationException e) {

        if (!(e instanceof ResteasyReactiveClientProblem)
            && e.getResponse() != null
            && e.getResponse().getStatus() == BAD_REQUEST.getStatusCode()) {

            final Throwable cause = e.getCause();
            var causeType = cause.getClass();

            do {
                if (KNOWN_BAD_REQUEST_TYPES.contains(causeType)) {
                    return createResponse(Status.BAD_REQUEST,
                                          (Exception) cause,
                                          GENERIC_BAD_REQUEST.getMessage(),
                                          GENERIC_BAD_REQUEST.getErrorCode());
                }

                causeType = (Class<? extends Throwable>) causeType.getSuperclass();
            } while (!causeType.equals(Throwable.class));
        } else if (e.getCause() instanceof IllegalArgumentException) {
            return createResponse(BAD_REQUEST, e, null, GENERIC_BAD_REQUEST.getErrorCode());
        }
        return createResponse(Status.INTERNAL_SERVER_ERROR, e, UNKNOWN_ERROR_WITHOUT_CAUSE.getMessage(), ErrorMessage.UNKNOWN_ERROR);
    }

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(ForbiddenException e) {
        return createResponse(Status.FORBIDDEN, e, null, USER_NOT_ALLOWED.getErrorCode());
    }

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(NotAcceptableException e) {
        return RestResponse.status(Status.NOT_ACCEPTABLE, ErrorResponse.builder().message(e.getMessage()).build());
    }

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(CompositePlatformException exception) {
        return createResponse(getHighestStatusCode(exception.getMappedStatusCodes()), exception);
    }
    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(ValidationException e) {
        return createResponse(BAD_REQUEST, e, e.getMessage(), GENERIC_BAD_REQUEST.getErrorCode());
    }

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(Exception e) {
        return createResponse(Status.INTERNAL_SERVER_ERROR, e, UNKNOWN_ERROR_WITHOUT_CAUSE.getMessage(), ErrorMessage.UNKNOWN_ERROR);
    }

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> map(PlatformException e) {
        LOG.errorf(e, LOG_PREFIX, e.getMessage());

        Status status = Status.fromStatusCode(e.getErrorMessage().getHttpStatusCode());

        return createResponse(status, e, e.getErrorMessage(), e.getErrorComplementaryInformation());
    }

    private RestResponse<ErrorResponse> createResponse(Status status, PlatformException e, ErrorMessage errorMessage) {
        return createResponse(status,
                              e == null ? errorMessage.getMessage() : errorMessage.getMessage(e.getAdditionalInformation()),
                              errorMessage.getErrorCode());
    }

    private RestResponse<ErrorResponse> createResponse(Status status,
                                                       PlatformException e,
                                                       ErrorMessage errorMessage,
                                                       Object complementaryInformation) {
        return createResponse(status,
                              e == null ? errorMessage.getMessage() : errorMessage.getMessage(e.getAdditionalInformation()),
                              errorMessage.getErrorCode(),
                              complementaryInformation);
    }

    private RestResponse<ErrorResponse> createResponse(Status status, Exception e, String customErrorMessage, String errorCode) {
        if (status.getFamily().equals(Family.SERVER_ERROR)) {
            LOG.errorf(e, LOG_PREFIX, e.getMessage());
        } else {
            LOG.infof(e, LOG_PREFIX, e.getMessage());
        }

        return (customErrorMessage != null)
            ? RestResponse.status(status, ErrorResponse.builder().message(customErrorMessage).errorCode(errorCode).build())
            : RestResponse.status(status, ErrorResponse.builder().message(e.getMessage()).errorCode(errorCode).build());
    }

    private RestResponse<ErrorResponse> createResponse(Status status, String errorMessage, String errorCode) {
        return RestResponse.status(
            status,
            ErrorResponse.builder()
                .message(errorMessage)
                .traceId(Span.current().getSpanContext().getTraceId())
                .errorCode(errorCode)
                .build()
        );
    }

    private RestResponse<ErrorResponse> createResponse(Status status,
                                                       String errorMessage,
                                                       String errorCode,
                                                       Object complementaryInformation) {
        return RestResponse.status(
            status,
            ErrorResponse.builder()
                .message(errorMessage)
                .traceId(Span.current().getSpanContext().getTraceId())
                .errorCode(errorCode)
                .complementaryInformation(complementaryInformation)
                .build()
        );
    }

    private RestResponse<ErrorResponse> createResponse(Status status, CompositePlatformException exception) {
        LOG.warnf("Multiple exceptions occurred:");
        exception.getExceptions().forEach(e -> log.warnf(e, e.getMessage()));
        String message = exception.getExceptions().stream().map(PlatformException::getMessage).distinct().collect(Collectors.joining(", "));
        return RestResponse.status(
            status,
            ErrorResponse.builder()
                .message(message)
                .traceId(Span.current().getSpanContext().getTraceId())
                .errorCode(exception.getErrorCode())
                .details(exception.getErrorDetails())
                .build()
        );
    }

    private Status getHighestStatusCode(Map<Status, List<PlatformException>> mappedStatusCodes) {
        if (mappedStatusCodes.containsKey(Status.FORBIDDEN)) {
            return Status.FORBIDDEN;
        }
        if (mappedStatusCodes.containsKey(Status.CONFLICT)) {
            return Status.CONFLICT;
        }
        if (mappedStatusCodes.containsKey(Status.NOT_FOUND)) {
            return Status.NOT_FOUND;
        }
        if (mappedStatusCodes.containsKey(Status.BAD_REQUEST)) {
            return Status.BAD_REQUEST;
        }
        return Status.INTERNAL_SERVER_ERROR;
    }

And this is the code for a failing test

@Test
@TestSecurity(user = "alice", roles = {"user"})
@OidcSecurity(claims = {
    @Claim(key = "sub", value = "someIdThatDoesNotAffectUs"),
    @Claim(key = "platform_user_id", value = "dead1234cafe123f2812f93f"),
    @Claim(key = "organization_id", value = ORGANIZATION_ID)
})
void testThatFails() {
    //given
    ObjectId cartridgeVariantId = new ObjectId();
    String cartridgeVariantIdString = cartridgeVariantId.toHexString();
    TestDataUtils.givenRequesterAsServiceAdmin(userRepository, userWithRolesRepository, cartridgeVariantIdString, false);
    var givenOrganization = givenOrganization(organizationRepository, new ObjectId(ORGANIZATION_ID), ORGANIZATION_NAME, QualificationLevel.LEVEL_2, null);
    givenUsageRight(usageRightRepository, new ObjectId(ORGANIZATION_ID), cartridgeVariantId);
    String newUserFirstName = "Martina";
    String newUserLastName = "Mustermann";
    String cartridgeRoleId = new ObjectId().toHexString();
    String groupId = new ObjectId().toHexString();
    UserJoinOrganizationInvitationRequestDto given = UserJoinOrganizationInvitationRequestDto
        .builder()
        .invitations(List.of(UserToOrganizationInvitationDto
                                 .builder()
                                 .email(TEST_USER_EMAIL_NEW)
                                 .organizationId(ORGANIZATION_ID)
                                 .salutation("w")
                                 .firstname(newUserFirstName)
                                 .lastname(newUserLastName)
                                 .organizationRole(ORGANIZATION_MEMBER)
                                 .cartridgeRoles(List.of(CartridgeRoleAssignmentRequestDto
                                                             .builder()
                                                             .roleId(cartridgeRoleId)
                                                             .cartridgeVariantId(cartridgeVariantIdString)
                                                             .build()))
                                 .department("MANAGEMENT")
                                 .position("CEO")
                                 .groupIds(Set.of(groupId))
                                 .build()))
        .build();

    journalTopic.clear();

    //when
    var actual = given()
        .body(given)
        .contentType(ContentType.JSON)
        .when()
        .post("/user/invitation")
        .then()
        .statusCode(StatusCode.CREATED)
        .extract()
        .as(InviteUsersResponseDto.class);
    //then

    assertThat(actual.getUsers()).singleElement().satisfies(user -> {
        assertThat(user.getEmail()).isEqualTo(TEST_USER_EMAIL_NEW);
        assertThat(user.getLastname()).isEqualTo(newUserLastName);
        assertThat(user.getFirstname()).isEqualTo(newUserFirstName);
        assertThat(user.getOrganizationMemberships())
            .singleElement()
            .satisfies(membership -> assertThat(membership.getOrganizationId()).isEqualTo(ORGANIZATION_ID));
    });
    assertThat(actual.getFailures()).isEmpty();

    Awaitility.await().atMost(TEST_TIMEOUT).untilAsserted(() -> {
        var allUsers = userRepository.listAll().await().atMost(TEST_TIMEOUT);
        assertThat(allUsers)
            .satisfiesExactlyInAnyOrder(actualRequester -> assertThat(actualRequester.getId()).isEqualTo(new ObjectId(REQUESTER_ID)),
                                        actualNewUser -> {
                                            assertThat(actualNewUser.getEmail()).isEqualTo(TEST_USER_EMAIL_NEW);
                                            assertThat(actualNewUser.getLastname()).isEqualTo(newUserLastName);
                                            assertThat(actualNewUser.getFirstname()).isEqualTo(newUserFirstName);
                                            assertThat(actualNewUser.getOrganizationMemberships()).singleElement().satisfies(membership -> {
                                                assertThat(membership.getOrganizationId()).isEqualTo(new ObjectId(ORGANIZATION_ID));
                                                assertThat(membership.getDepartment()).isEqualTo(DepartmentAffiliation.MANAGEMENT);
                                                assertThat(membership.getPosition()).isEqualTo("CEO");
                                                assertThat(membership.getUserOrganizationStatus()).isEqualTo(UserOrganizationStatus.INVITED);
                                            });
                                        });
        var actualNewUser = allUsers.stream().filter(u -> TEST_USER_EMAIL_NEW.equals(u.getEmail())).findAny().orElseThrow();

        List<? extends Message<PlattformMessage>> actualPublishedAuthorizerCommands = authorizerCommandQueue.received();
        assertThat(actualPublishedAuthorizerCommands.stream().map(Message::getPayload)).singleElement().satisfies(payload -> {
            assertThat(payload).isInstanceOf(AssignRoleAndGroupsToNewOrganizationMemberCommand.class);
            AssignRoleAndGroupsToNewOrganizationMemberCommand command = (AssignRoleAndGroupsToNewOrganizationMemberCommand) payload;
            assertThat(command.requesterId()).isEqualTo(REQUESTER_ID);
            assertThat(command.userId()).isEqualTo(actualNewUser.getId().toHexString());
            assertThat(command.organizationId()).isEqualTo(ORGANIZATION_ID);
            assertThat(command.organizationRole()).isEqualTo(
                ORGANIZATION_MEMBER);
            assertThat(command.cartridgeRoles())
                .singleElement()
                .isEqualTo(CartridgeRoleAssignmentRequestDto
                               .builder()
                               .roleId(cartridgeRoleId)
                               .cartridgeVariantId(cartridgeVariantIdString)
                               .build());
            assertThat(command.groupIds()).singleElement().isEqualTo(groupId);
        });

        List<? extends Message<PlattformMessage>> actualPublishedEvents = userEventTopic.received();
        assertThat(actualPublishedEvents.stream().map(Message::getPayload))
            .hasSize(3)
            .satisfiesExactlyInAnyOrder(event -> assertOrganizationMembershipCreatedEvent(event, actualNewUser),
                                        event -> assertionsPayloadOrganizationMembershipUpdatedEvent(new ObjectId(ORGANIZATION_ID),
                                                                                                     actualNewUser.getId(),
                                                                                                     actualNewUser.getEmail(),
                                                                                                     UserOrganizationStatus.INVITED.toString(),
                                                                                                     event,
                                                                                                     actualNewUser.getVersion(), null,
                                                                                                     givenOrganization.getQualificationLevel()),
                                        event -> assertUserCreatedEvent(event, actualNewUser, TEST_USER_EMAIL_NEW));

        Map<String, AuditChangeEntry> expectedChangeSet = new HashMap<>();
        expectedChangeSet.put(ORGANIZATION_MEMBERSHIP_FORMAT.formatted(ORGANIZATION_MEMBERSHIP_IDENTIFIER, ORGANIZATION_ID,
                                                                       USER_ORGANIZATION_STATUS_IDENTIFIER),
                              new AuditChangeEntry(null, UserOrganizationStatus.INVITED.name()));
        String expectedReason = "User was invited";
        List<? extends Message<AuditEvent>> auditEvents = journalTopic.received();
        assertThat(auditEvents)
            .hasSize(1)
            .map(Message::getPayload)
            .extracting("operationType", "entityType", "entityId", "version", "changeSet", "reason")
            .containsExactlyInAnyOrder(tuple(CREATE, "User", actualNewUser.getId().toHexString(), 1, expectedChangeSet, expectedReason));

        List<KpiMessage> actualPublishedKpiEvents = serviceBusTestUtils.receiveFromTopicWithTimeout(messagingReceiverClient,
                                                                                                    ANALYTICS_TOPIC,
                                                                                                    SUBSCRIPTION_NAME,
                                                                                                    KpiMessage.class,
                                                                                                    TOPIC_RECEIVE_TIMEOUT);
        assertThat(actualPublishedKpiEvents).singleElement().satisfies(kpiEvent -> {
            assertThat(kpiEvent.userId()).isEqualTo(REQUESTER_ID);
            assertThat(kpiEvent.payload().get("userId")).isEqualTo(actualNewUser.getId().toHexString());
            assertThat(kpiEvent.organizationId()).isEqualTo(ORGANIZATION_ID);
            assertThat(kpiEvent.eventType()).isEqualTo(ORGANIZATION_MEMBERSHIP_CREATED.getValue());
        });

        Mockito
            .verify(mailService, Mockito.times(1))
            .sendInviteUnregisteredUserToOrganizationMail(eq(TEST_USER_EMAIL_NEW),
                                                          eq(newUserFirstName),
                                                          eq(newUserLastName),
                                                          eq(ORGANIZATION_NAME),
                                                          any(),
                                                          any());
    });
}

Edit One of my colleagues did smaller increment updates and we found something interesting.

3.21.4 -> 3.22.1 OK
3.22.1 -> 3.22.2 OK
3.22.2 -> 3.22.3 NOK

So whatever causes this happened in a PATCH update... which just boggles the mind, but if this helps someone help us then all the better.


Solution

  • We figure out how to get around this, we don't know if there would be alternatives but we are satisfied with the existing one so we will not pursue any further investigation for now.

    So if anyone runs into this thread at some point and has the same issue here's how we got around it.

    We created a wrapper for managing context:

    @ApplicationScoped
    public class ArcContextWrapper {
    
        public ManagedContext getRequestContext() {
            return Arc.container().requestContext();
        }
    
    }
    

    Then we introduced this in the method we figured out was causing the issue

    ManagedContext ctx = arcContextWrapper.getRequestContext();
    ctx.activate();
    

    This method had a lot of reactive calls in chain that used multiple threads, which was causing it the context to get lost.
    To ensure this works properly we added this at the end of the chain (that starts with deferred)

    .eventually(ctx::terminate)
    

    That exception I reported no longer happens after this change and the app is behaving normally.