This is an automated email from the ASF dual-hosted git repository.

kfaraz pushed a commit to branch 38.0.0
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/38.0.0 by this push:
     new 0bfde5a7f4f fix: ensure native query statusCode is 403 for blocklist 
(#19745)
0bfde5a7f4f is described below

commit 0bfde5a7f4ff5fa3454f147bd3cd5b41f9fbd448
Author: jtuglu1 <[email protected]>
AuthorDate: Wed Jul 29 22:32:31 2026 -0700

    fix: ensure native query statusCode is 403 for blocklist (#19745)
    
    This makes sure that any DruidExceptions thrown during query processing are 
correctly propagated with the correct status code for native queries.
    
    (cherry picked from commit afcee5a075bfc64179488e9ad22ce30b15ca0d85)
---
 .../server/EmbeddedBrokerDynamicConfigTest.java    |  51 +++
 .../exception/ErrorResponseTransformStrategy.java  |  18 +-
 .../PersonaBasedErrorTransformStrategy.java        |   2 +-
 .../PersonaBasedErrorTransformStrategyTest.java    |  31 +-
 .../apache/druid/server/BrokerQueryResource.java   |   5 +-
 .../org/apache/druid/server/QueryLifecycle.java    |   5 +-
 .../org/apache/druid/server/QueryResource.java     |  56 ++-
 .../org/apache/druid/server/QueryResultPusher.java |   7 +-
 .../druid/server/initialization/ServerConfig.java  |   6 +
 .../org/apache/druid/server/QueryResourceTest.java | 504 ++++++++++++++-------
 .../org/apache/druid/sql/avatica/ErrorHandler.java |   7 +-
 .../org/apache/druid/sql/http/SqlResource.java     |  52 ++-
 12 files changed, 519 insertions(+), 225 deletions(-)

diff --git 
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/EmbeddedBrokerDynamicConfigTest.java
 
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/EmbeddedBrokerDynamicConfigTest.java
index ac48e2c5346..dab43be7a45 100644
--- 
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/EmbeddedBrokerDynamicConfigTest.java
+++ 
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/EmbeddedBrokerDynamicConfigTest.java
@@ -23,6 +23,7 @@ import org.apache.druid.audit.AuditInfo;
 import org.apache.druid.common.config.JacksonConfigManager;
 import org.apache.druid.common.utils.IdUtils;
 import org.apache.druid.indexing.common.task.TaskBuilder;
+import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.query.QueryContext;
 import org.apache.druid.server.DefaultQueryBlocklistRule;
 import org.apache.druid.server.QueryBlocklistRule;
@@ -43,6 +44,11 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
 
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -127,6 +133,51 @@ public class EmbeddedBrokerDynamicConfigTest extends 
EmbeddedClusterTestBase
     Assertions.assertFalse(finalResult.isBlank());
   }
 
+  /**
+   * A blocklisted query must be rejected with 403.
+   */
+  @Test
+  @Timeout(30)
+  public void testBlocklistedQueryReturnsForbiddenOnBothEndpoints() throws 
Exception
+  {
+    updateBrokerDynamicConfig(
+        BrokerDynamicConfig.builder()
+                           .withQueryBlocklist(List.of(
+                               new 
DefaultQueryBlocklistRule("block-test-datasource", Set.of(dataSource), null, 
null)
+                           ))
+                           .build()
+    );
+
+    try {
+      final String nativeQuery = StringUtils.format(
+          
"{\"queryType\":\"timeseries\",\"dataSource\":\"%s\",\"granularity\":\"all\","
+          + 
"\"intervals\":[\"2000/3000\"],\"aggregations\":[{\"type\":\"count\",\"name\":\"rows\"}]}",
+          dataSource
+      );
+      Assertions.assertEquals(403, postToBroker("/druid/v2", 
nativeQuery).statusCode());
+
+      final String sqlQuery = StringUtils.format("{\"query\":\"SELECT COUNT(*) 
FROM \\\"%s\\\"\"}", dataSource);
+      Assertions.assertEquals(403, postToBroker("/druid/v2/sql", 
sqlQuery).statusCode());
+    }
+    finally {
+      updateBrokerDynamicConfig(BrokerDynamicConfig.builder().build());
+    }
+  }
+
+  private HttpResponse<String> postToBroker(String path, String body) throws 
Exception
+  {
+    final HttpRequest request = HttpRequest
+        .newBuilder(URI.create(getServerUrl(broker) + path))
+        .header("Content-Type", "application/json")
+        .timeout(Duration.ofSeconds(10))
+        .POST(HttpRequest.BodyPublishers.ofString(body))
+        .build();
+
+    try (HttpClient client = HttpClient.newHttpClient()) {
+      return client.send(request, HttpResponse.BodyHandlers.ofString());
+    }
+  }
+
   @Test
   @Timeout(30)
   public void testDynamicQueryContextTimeoutCausesQueryToFail()
diff --git 
a/processing/src/main/java/org/apache/druid/common/exception/ErrorResponseTransformStrategy.java
 
b/processing/src/main/java/org/apache/druid/common/exception/ErrorResponseTransformStrategy.java
index b48ba97ca90..807926f6f69 100644
--- 
a/processing/src/main/java/org/apache/druid/common/exception/ErrorResponseTransformStrategy.java
+++ 
b/processing/src/main/java/org/apache/druid/common/exception/ErrorResponseTransformStrategy.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import org.apache.druid.error.DruidException;
 
+import javax.annotation.Nullable;
 import javax.validation.constraints.NotNull;
 import java.util.Optional;
 import java.util.function.Function;
@@ -50,11 +51,26 @@ public interface ErrorResponseTransformStrategy
    * It is the callers responsibility to do so. Returns Optional.empty() if no 
transformation was applied.
    * The errorId is provided to be used in the transformed Exception if needed.
    */
-  default Optional<Exception> maybeTransform(DruidException exception, 
Optional<String> errorId)
+  default Optional<DruidException> maybeTransform(DruidException exception, 
Optional<String> errorId)
   {
     return Optional.empty();
   }
 
+  /**
+   * Applies {@link #maybeTransform} and returns the exception to hand back to 
the caller, or {@code exception} unchanged
+   * if this strategy does not transform it.
+   * <p>
+   * A transformed exception carries only {@code errorId}, not the original 
message, so callers are responsible for
+   * logging {@code exception} against {@code errorId}. Note also that the 
transformed exception carries its own category,
+   * so the status code the caller sees may differ from {@code 
exception.getStatusCode()}.
+   *
+   * @param errorId id echoed back to the caller by the transformed exception; 
a random one is used if null
+   */
+  default DruidException sanitizeForClient(DruidException exception, @Nullable 
String errorId)
+  {
+    return maybeTransform(exception, 
Optional.ofNullable(errorId)).orElse(exception);
+  }
+
   /**
    * Return a function for checking and transforming the error message if 
needed.
    * Function can return null if error message needs to be omitted or return 
String to be use instead.
diff --git 
a/processing/src/main/java/org/apache/druid/common/exception/PersonaBasedErrorTransformStrategy.java
 
b/processing/src/main/java/org/apache/druid/common/exception/PersonaBasedErrorTransformStrategy.java
index a0122204f79..3f83dc592f1 100644
--- 
a/processing/src/main/java/org/apache/druid/common/exception/PersonaBasedErrorTransformStrategy.java
+++ 
b/processing/src/main/java/org/apache/druid/common/exception/PersonaBasedErrorTransformStrategy.java
@@ -42,7 +42,7 @@ public class PersonaBasedErrorTransformStrategy implements 
ErrorResponseTransfor
    * exception was modified. Returns an empty optional if no transformation 
was performed.
    */
   @Override
-  public Optional<Exception> maybeTransform(DruidException druidException, 
Optional<String> optionalErrorId)
+  public Optional<DruidException> maybeTransform(DruidException 
druidException, Optional<String> optionalErrorId)
   {
     if (druidException.getTargetPersona() == DruidException.Persona.USER) {
       return Optional.empty();
diff --git 
a/processing/src/test/java/org/apache/druid/common/exception/PersonaBasedErrorTransformStrategyTest.java
 
b/processing/src/test/java/org/apache/druid/common/exception/PersonaBasedErrorTransformStrategyTest.java
index 068fb30ab9c..fc6f446b18c 100644
--- 
a/processing/src/test/java/org/apache/druid/common/exception/PersonaBasedErrorTransformStrategyTest.java
+++ 
b/processing/src/test/java/org/apache/druid/common/exception/PersonaBasedErrorTransformStrategyTest.java
@@ -22,6 +22,7 @@ package org.apache.druid.common.exception;
 import nl.jqno.equalsverifier.EqualsVerifier;
 import org.apache.druid.error.DruidException;
 import org.apache.druid.error.DruidExceptionMatcher;
+import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -52,13 +53,31 @@ public class PersonaBasedErrorTransformStrategyTest
   {
     DruidException druidException = DruidException.defensive().build("Test 
Defensive exception");
 
-    DruidExceptionMatcher druidExceptionMatcher = new DruidExceptionMatcher(
-        DruidException.Persona.USER,
-        druidException.getCategory(),
-        druidException.getErrorCode()
-    ).expectMessageContains("Could not process the query, please contact your 
administrator with Error ID");
+    MatcherAssert.assertThat(
+        target.maybeTransform(druidException, Optional.of("the-error")).get(),
+        new DruidExceptionMatcher(
+            DruidException.Persona.USER,
+            DruidException.Category.RUNTIME_FAILURE,
+            "general"
+        ).expectMessageIs(
+            "Internal server error, please contact your administrator with 
Error ID [the-error] if the issue persists."
+        )
+    );
+  }
+
+  @Test
+  public void testErrorIdIsGeneratedWhenAbsent()
+  {
+    DruidException druidException = DruidException.defensive().build("Test 
Defensive exception");
 
-    druidExceptionMatcher.matches(target.maybeTransform(druidException, 
Optional.of("the-error")).get());
+    MatcherAssert.assertThat(
+        target.maybeTransform(druidException, Optional.empty()).get(),
+        new DruidExceptionMatcher(
+            DruidException.Persona.USER,
+            DruidException.Category.RUNTIME_FAILURE,
+            "general"
+        ).expectMessageContains("please contact your administrator with Error 
ID [")
+    );
   }
 
   @Test
diff --git 
a/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java 
b/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java
index c5638561ed2..ad3b18b9ec0 100644
--- a/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java
+++ b/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java
@@ -31,6 +31,7 @@ import org.apache.druid.query.Query;
 import org.apache.druid.query.QueryContexts;
 import org.apache.druid.query.planning.ExecutionVertex;
 import org.apache.druid.server.http.security.StateResourceFilter;
+import org.apache.druid.server.initialization.ServerConfig;
 import org.apache.druid.server.security.AuthorizerMapper;
 
 import javax.annotation.Nullable;
@@ -62,6 +63,7 @@ public class BrokerQueryResource extends QueryResource
       AuthorizerMapper authorizerMapper,
       QueryResourceQueryResultPusherFactory queryResultPusherFactory,
       ResourceIOReaderWriterFactory resourceIOReaderWriterFactory,
+      ServerConfig serverConfig,
       TimelineServerView brokerServerView
   )
   {
@@ -71,7 +73,8 @@ public class BrokerQueryResource extends QueryResource
         queryScheduler,
         authorizerMapper,
         queryResultPusherFactory,
-        resourceIOReaderWriterFactory
+        resourceIOReaderWriterFactory,
+        serverConfig
     );
     this.brokerServerView = brokerServerView;
   }
diff --git a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java 
b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
index 9a514b85e2d..fc7d896df83 100644
--- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
+++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
@@ -389,6 +389,10 @@ public class QueryLifecycle
     Preconditions.checkNotNull(authenticationResult, "authenticationResult");
     Preconditions.checkNotNull(authorizationResult, "authorizationResult");
 
+    // Authentication has already happened, so record the identity before 
anything below can throw. Note this means
+    // a set authenticationResult implies only that authorization was 
attempted, not that it succeeded.
+    this.authenticationResult = authenticationResult;
+
     if (!authorizationResult.allowBasicAccess()) {
       // Not authorized; go straight to Jail, do not pass Go.
       transition(State.AUTHORIZING, State.UNAUTHORIZED);
@@ -405,7 +409,6 @@ public class QueryLifecycle
                                                               ));
     }
 
-    this.authenticationResult = authenticationResult;
     return authorizationResult;
   }
 
diff --git a/server/src/main/java/org/apache/druid/server/QueryResource.java 
b/server/src/main/java/org/apache/druid/server/QueryResource.java
index 80ea2895b97..ad96c20f32e 100644
--- a/server/src/main/java/org/apache/druid/server/QueryResource.java
+++ b/server/src/main/java/org/apache/druid/server/QueryResource.java
@@ -25,8 +25,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializerProvider;
 import com.fasterxml.jackson.jaxrs.smile.SmileMediaTypes;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
+import org.apache.druid.error.DruidException;
+import org.apache.druid.error.QueryExceptionCompat;
 import org.apache.druid.guice.LazySingleton;
 import org.apache.druid.guice.annotations.Json;
 import org.apache.druid.java.util.common.jackson.JacksonUtils;
@@ -38,6 +41,7 @@ import org.apache.druid.query.QueryException;
 import org.apache.druid.query.QueryInterruptedException;
 import org.apache.druid.query.context.ResponseContext;
 import org.apache.druid.query.context.ResponseContext.Keys;
+import org.apache.druid.server.initialization.ServerConfig;
 import org.apache.druid.server.metrics.QueryCountStatsProvider;
 import org.apache.druid.server.security.AuthorizationResult;
 import org.apache.druid.server.security.AuthorizationUtils;
@@ -91,6 +95,7 @@ public class QueryResource implements QueryCountStatsProvider
   protected final QueryScheduler queryScheduler;
   protected final AuthorizerMapper authorizerMapper;
 
+  private final ServerConfig serverConfig;
   private final QueryResourceQueryResultPusherFactory queryResultPusherFactory;
   protected final ResourceIOReaderWriterFactory resourceIOReaderWriterFactory;
 
@@ -107,7 +112,8 @@ public class QueryResource implements 
QueryCountStatsProvider
       QueryScheduler queryScheduler,
       AuthorizerMapper authorizerMapper,
       QueryResourceQueryResultPusherFactory queryResultPusherFactory,
-      ResourceIOReaderWriterFactory resourceIOReaderWriterFactory
+      ResourceIOReaderWriterFactory resourceIOReaderWriterFactory,
+      ServerConfig serverConfig
   )
   {
     this.queryLifecycleFactory = queryLifecycleFactory;
@@ -116,6 +122,7 @@ public class QueryResource implements 
QueryCountStatsProvider
     this.authorizerMapper = authorizerMapper;
     this.queryResultPusherFactory = queryResultPusherFactory;
     this.resourceIOReaderWriterFactory = resourceIOReaderWriterFactory;
+    this.serverConfig = serverConfig;
   }
 
   @DELETE
@@ -182,15 +189,7 @@ public class QueryResource implements 
QueryCountStatsProvider
         authResult = queryLifecycle.authorize(req);
       }
       catch (RuntimeException e) {
-        final QueryException qe;
-
-        if (e instanceof QueryException) {
-          qe = (QueryException) e;
-        } else {
-          qe = new QueryInterruptedException(e);
-        }
-
-        return 
io.getResponseWriter().buildNonOkResponse(qe.getFailType().getExpectedStatus(), 
qe);
+        return handleAuthorizeFailure(queryLifecycle, io, req, e);
       }
 
       if (!authResult.allowBasicAccess()) {
@@ -239,6 +238,43 @@ public class QueryResource implements 
QueryCountStatsProvider
     }
   }
 
+  /**
+   * Builds the response for a query that failed during {@link 
QueryLifecycle#authorize}, before a
+   * {@link QueryResultPusher} exists to do it. The pusher is what normally 
records the failure, so this has to emit the
+   * logs, metrics and counters itself; otherwise the caller gets a response 
but nothing is recorded server-side.
+   * <p>
+   * A {@link DruidException} is reported under its own category. Everything 
else goes through the {@link QueryException}
+   * conversion below, which resolves anything it does not recognise to
+   * {@link QueryException#UNKNOWN_EXCEPTION_ERROR_CODE} and therefore a 500.
+   */
+  private Response handleAuthorizeFailure(
+      final QueryLifecycle queryLifecycle,
+      final ResourceIOReaderWriterFactory.ResourceIOReaderWriter io,
+      final HttpServletRequest req,
+      final RuntimeException e
+  ) throws IOException
+  {
+    // Logs the exception with the query id, which doubles as the error id 
below.
+    queryLifecycle.emitLogsAndMetrics(e, req.getRemoteAddr(), -1);
+
+    if (e instanceof DruidException) {
+      final DruidException druidException = (DruidException) e;
+      QueryResultPusher.incrementQueryCounterForException(counter, 
druidException);
+
+      final String queryId = queryLifecycle.getQueryId();
+      return QueryResultPusher.handleDruidExceptionBeforeResponseStarted(
+          
serverConfig.getErrorResponseTransformStrategy().sanitizeForClient(druidException,
 queryId),
+          MediaType.valueOf(io.getResponseWriter().getResponseType()),
+          ImmutableMap.of(QUERY_ID_RESPONSE_HEADER, queryId)
+      );
+    }
+
+    final QueryException qe = e instanceof QueryException ? (QueryException) e 
: new QueryInterruptedException(e);
+    // Converted only to reuse the category mapping; the response body stays 
in the legacy QueryException format.
+    QueryResultPusher.incrementQueryCounterForException(counter, 
DruidException.fromFailure(new QueryExceptionCompat(qe)));
+    return 
io.getResponseWriter().buildNonOkResponse(qe.getFailType().getExpectedStatus(), 
qe);
+  }
+
   public interface QueryMetricCounter
   {
     void incrementSuccess();
diff --git 
a/server/src/main/java/org/apache/druid/server/QueryResultPusher.java 
b/server/src/main/java/org/apache/druid/server/QueryResultPusher.java
index ee9849a7b11..cb958ea9857 100644
--- a/server/src/main/java/org/apache/druid/server/QueryResultPusher.java
+++ b/server/src/main/java/org/apache/druid/server/QueryResultPusher.java
@@ -228,7 +228,10 @@ public abstract class QueryResultPusher
     return handleDruidException(resultsWriter, DruidException.fromFailure(new 
QueryExceptionCompat(e)));
   }
 
-  private void incrementQueryCounterForException(final DruidException e)
+  static void incrementQueryCounterForException(
+      final QueryResource.QueryMetricCounter counter,
+      final DruidException e
+  )
   {
     switch (e.getCategory()) {
       case INVALID_INPUT:
@@ -251,7 +254,7 @@ public abstract class QueryResultPusher
 
   private Response handleDruidException(ResultsWriter resultsWriter, 
DruidException e)
   {
-    incrementQueryCounterForException(e);
+    incrementQueryCounterForException(counter, e);
 
     if (resultsWriter != null) {
       final long bytesWritten = accumulator != null ? 
accumulator.getNumBytesSent() : 0;
diff --git 
a/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java 
b/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java
index a7206ca9cf7..92fa8dfd0a5 100644
--- 
a/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java
+++ 
b/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java
@@ -124,6 +124,12 @@ public class ServerConfig
     this.enableQueryRequestsQueuing = enableQueryRequestsQueuing;
   }
 
+  @VisibleForTesting
+  public ServerConfig(@NotNull ErrorResponseTransformStrategy 
errorResponseTransformStrategy)
+  {
+    this.errorResponseTransformStrategy = errorResponseTransformStrategy;
+  }
+
   @JsonProperty
   @Min(1)
   private int numThreads = getDefaultNumThreads();
diff --git 
a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java 
b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
index d7a611bfc32..5d4ac54b99c 100644
--- a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
+++ b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
@@ -31,6 +31,8 @@ import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.inject.Injector;
 import com.google.inject.Key;
+import org.apache.druid.client.BrokerViewOfBrokerConfig;
+import org.apache.druid.common.exception.ErrorResponseTransformStrategy;
 import org.apache.druid.error.DruidException;
 import org.apache.druid.error.DruidExceptionMatcher;
 import org.apache.druid.error.ErrorResponse;
@@ -40,6 +42,7 @@ import org.apache.druid.guice.annotations.Smile;
 import org.apache.druid.jackson.DefaultObjectMapper;
 import org.apache.druid.java.util.common.DateTimes;
 import org.apache.druid.java.util.common.Intervals;
+import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.java.util.common.concurrent.Execs;
 import org.apache.druid.java.util.common.guava.Accumulator;
 import org.apache.druid.java.util.common.guava.BaseSequence;
@@ -74,6 +77,7 @@ import org.apache.druid.query.filter.NullFilter;
 import org.apache.druid.query.policy.NoopPolicyEnforcer;
 import org.apache.druid.query.policy.RowFilterPolicy;
 import org.apache.druid.query.timeboundary.TimeBoundaryResultValue;
+import org.apache.druid.server.broker.BrokerDynamicConfig;
 import org.apache.druid.server.initialization.ServerConfig;
 import org.apache.druid.server.log.TestRequestLogger;
 import org.apache.druid.server.metrics.NoopServiceEmitter;
@@ -103,9 +107,12 @@ import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.internal.matchers.ThrowableMessageMatcher;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -122,12 +129,14 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 public class QueryResourceTest
@@ -261,33 +270,68 @@ public class QueryResourceTest
     queryResource = createQueryResource(ResponseContextConfig.newConfig(true));
   }
 
+  private QueryLifecycleFactory createQueryLifecycleFactory()
+  {
+    return new QueryLifecycleFactory(
+        CONGLOMERATE,
+        TEST_SEGMENT_WALKER,
+        new DefaultGenericQueryMetricsFactory(),
+        emitter,
+        testRequestLogger,
+        new AuthConfig(),
+        NoopPolicyEnforcer.instance(),
+        AuthTestUtils.TEST_AUTHORIZER_MAPPER,
+        new DefaultQueryConfig(Map.of()),
+        null
+    );
+  }
+
   private QueryResource createQueryResource(ResponseContextConfig 
responseContextConfig)
+  {
+    return createQueryResource(
+        createQueryLifecycleFactory(),
+        null,
+        queryScheduler,
+        responseContextConfig,
+        smileMapper,
+        new ServerConfig()
+    );
+  }
+
+  private QueryResource createQueryResource(QueryLifecycleFactory 
queryLifecycleFactory)
+  {
+    return createQueryResource(
+        queryLifecycleFactory,
+        null,
+        queryScheduler,
+        ResponseContextConfig.newConfig(true),
+        smileMapper,
+        new ServerConfig()
+    );
+  }
+
+  /**
+   * Every {@link QueryResource} under test is built here, so a change to its 
constructor touches one call site.
+   *
+   * @param responseMapper mapper backing the response writer, i.e. the smile 
mapper unless the test needs json
+   */
+  private QueryResource createQueryResource(
+      final QueryLifecycleFactory queryLifecycleFactory,
+      @Nullable final AuthorizerMapper authorizerMapper,
+      final QueryScheduler queryScheduler,
+      final ResponseContextConfig responseContextConfig,
+      final ObjectMapper responseMapper,
+      final ServerConfig serverConfig
+  )
   {
     return new QueryResource(
-        new QueryLifecycleFactory(
-            CONGLOMERATE,
-            TEST_SEGMENT_WALKER,
-            new DefaultGenericQueryMetricsFactory(),
-            emitter,
-            testRequestLogger,
-            new AuthConfig(),
-            NoopPolicyEnforcer.instance(),
-            AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-            new DefaultQueryConfig(Map.of()),
-            null
-        ),
+        queryLifecycleFactory,
         jsonMapper,
         queryScheduler,
-        null,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            responseContextConfig,
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(
-            jsonMapper,
-            smileMapper
-        )
+        authorizerMapper,
+        new QueryResourceQueryResultPusherFactory(jsonMapper, 
responseContextConfig, DRUID_NODE),
+        new ResourceIOReaderWriterFactory(jsonMapper, responseMapper),
+        serverConfig
     );
   }
 
@@ -313,7 +357,7 @@ public class QueryResourceTest
     final String overrideConfigKey = "priority";
     final String overrideConfigValue = "678";
     DefaultQueryConfig overrideConfig = new 
DefaultQueryConfig(ImmutableMap.of(overrideConfigKey, overrideConfigValue));
-    queryResource = new QueryResource(
+    queryResource = createQueryResource(
         new QueryLifecycleFactory(
             CONGLOMERATE,
             TEST_SEGMENT_WALKER,
@@ -325,18 +369,6 @@ public class QueryResourceTest
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
             overrideConfig,
             null
-        ),
-        jsonMapper,
-        queryScheduler,
-        null,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(
-            jsonMapper,
-            smileMapper
         )
     );
 
@@ -367,13 +399,13 @@ public class QueryResourceTest
     );
   }
 
-  @Test
-  public void testGoodQueryThrowsDruidExceptionFromLifecycleExecute() throws 
IOException
+  /**
+   * A {@link QueryResource} whose walker throws once the query is already 
executing, so the failure surfaces through
+   * {@link QueryResultPusher} rather than before it.
+   */
+  private QueryResource createQueryResourceFailingInExecute(final 
DefaultQueryConfig queryConfig)
   {
-    String overrideConfigKey = "priority";
-    String overrideConfigValue = "678";
-    DefaultQueryConfig overrideConfig = new 
DefaultQueryConfig(ImmutableMap.of(overrideConfigKey, overrideConfigValue));
-    queryResource = new QueryResource(
+    return createQueryResource(
         new QueryLifecycleFactory(
             CONGLOMERATE,
             new QuerySegmentWalker()
@@ -404,22 +436,19 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-            overrideConfig,
+            queryConfig,
             null
-        ),
-        jsonMapper,
-        queryScheduler,
-        null,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(
-            jsonMapper,
-            smileMapper
         )
     );
+  }
+
+  @Test
+  public void testGoodQueryThrowsDruidExceptionFromLifecycleExecute() throws 
IOException
+  {
+    String overrideConfigKey = "priority";
+    String overrideConfigValue = "678";
+    DefaultQueryConfig overrideConfig = new 
DefaultQueryConfig(ImmutableMap.of(overrideConfigKey, overrideConfigValue));
+    queryResource = createQueryResourceFailingInExecute(overrideConfig);
 
     expectPermissiveHappyPathAuth();
 
@@ -452,7 +481,7 @@ public class QueryResourceTest
   @Test
   public void testResponseWithIncludeTrailerHeader() throws IOException
   {
-    queryResource = new QueryResource(
+    queryResource = createQueryResource(
         new QueryLifecycleFactory(
             CONGLOMERATE,
             new QuerySegmentWalker()
@@ -507,16 +536,7 @@ public class QueryResourceTest
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
             new DefaultQueryConfig(Map.of()),
             null
-        ),
-        jsonMapper,
-        queryScheduler,
-        null,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(jsonMapper, smileMapper)
+        )
     );
 
     expectPermissiveHappyPathAuth();
@@ -543,7 +563,7 @@ public class QueryResourceTest
   @Test
   public void testResponseWithMidFlightExceptions() throws IOException
   {
-    queryResource = new QueryResource(
+    queryResource = createQueryResource(
         new QueryLifecycleFactory(
             CONGLOMERATE,
             new QuerySegmentWalker()
@@ -593,16 +613,7 @@ public class QueryResourceTest
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
             new DefaultQueryConfig(Map.of()),
             null
-        ),
-        jsonMapper,
-        queryScheduler,
-        null,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(jsonMapper, smileMapper)
+        )
     );
 
     expectPermissiveHappyPathAuth();
@@ -630,7 +641,7 @@ public class QueryResourceTest
         Intervals.of("2025-01-01/P1D"), "0", 1
     );
 
-    queryResource = new QueryResource(
+    queryResource = createQueryResource(
         new QueryLifecycleFactory(
             CONGLOMERATE,
             new QuerySegmentWalker()
@@ -692,18 +703,6 @@ public class QueryResourceTest
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
             new DefaultQueryConfig(Map.of()),
             null
-        ),
-        jsonMapper,
-        queryScheduler,
-        null,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(
-            jsonMapper,
-            smileMapper
         )
     );
 
@@ -758,7 +757,7 @@ public class QueryResourceTest
       }
     };
 
-    queryResource = new QueryResource(
+    queryResource = createQueryResource(
 
         new QueryLifecycleFactory(null, null, null, null, null, null, 
NoopPolicyEnforcer.instance(), null, overrideConfig, null)
         {
@@ -789,19 +788,7 @@ public class QueryResourceTest
               }
             };
           }
-        },
-        jsonMapper,
-        queryScheduler,
-        null,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(
-            jsonMapper,
-            smileMapper
-        )
+        }
     );
 
     expectPermissiveHappyPathAuth();
@@ -828,7 +815,7 @@ public class QueryResourceTest
     String overrideConfigKey = "priority";
     String overrideConfigValue = "678";
     DefaultQueryConfig overrideConfig = new 
DefaultQueryConfig(ImmutableMap.of(overrideConfigKey, overrideConfigValue));
-    queryResource = new QueryResource(
+    queryResource = createQueryResource(
         new QueryLifecycleFactory(
             CONGLOMERATE,
             TEST_SEGMENT_WALKER,
@@ -840,18 +827,6 @@ public class QueryResourceTest
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
             overrideConfig,
             null
-        ),
-        jsonMapper,
-        queryScheduler,
-        null,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(
-            jsonMapper,
-            smileMapper
         )
     );
 
@@ -1100,7 +1075,7 @@ public class QueryResourceTest
       }
     };
 
-    queryResource = new QueryResource(
+    queryResource = createQueryResource(
         new QueryLifecycleFactory(
             CONGLOMERATE,
             TEST_SEGMENT_WALKER,
@@ -1113,18 +1088,11 @@ public class QueryResourceTest
             new DefaultQueryConfig(Map.of()),
             null
         ),
-        jsonMapper,
-        queryScheduler,
         authMapper,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(
-            jsonMapper,
-            smileMapper
-        )
+        queryScheduler,
+        ResponseContextConfig.newConfig(true),
+        smileMapper,
+        new ServerConfig()
     );
 
 
@@ -1182,7 +1150,7 @@ public class QueryResourceTest
       }
     };
 
-    final QueryResource timeoutQueryResource = new QueryResource(
+    final QueryResource timeoutQueryResource = createQueryResource(
         new QueryLifecycleFactory(
             CONGLOMERATE,
             timeoutSegmentWalker,
@@ -1195,18 +1163,11 @@ public class QueryResourceTest
             new DefaultQueryConfig(Map.of()),
             null
         ),
-        jsonMapper,
-        queryScheduler,
         null,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(
-            jsonMapper,
-            jsonMapper
-        )
+        queryScheduler,
+        ResponseContextConfig.newConfig(true),
+        jsonMapper,
+        new ServerConfig()
     );
     expectPermissiveHappyPathAuth();
 
@@ -1287,7 +1248,7 @@ public class QueryResourceTest
       }
     };
 
-    queryResource = new QueryResource(
+    queryResource = createQueryResource(
         new QueryLifecycleFactory(
             CONGLOMERATE,
             TEST_SEGMENT_WALKER,
@@ -1300,18 +1261,11 @@ public class QueryResourceTest
             new DefaultQueryConfig(Map.of()),
             null
         ),
-        jsonMapper,
-        queryScheduler,
         authMapper,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(
-            jsonMapper,
-            smileMapper
-        )
+        queryScheduler,
+        ResponseContextConfig.newConfig(true),
+        smileMapper,
+        new ServerConfig()
     );
 
     final String queryString = "{\"queryType\":\"timeBoundary\", 
\"dataSource\":\"allow\","
@@ -1401,7 +1355,7 @@ public class QueryResourceTest
       }
     };
 
-    queryResource = new QueryResource(
+    queryResource = createQueryResource(
         new QueryLifecycleFactory(
             CONGLOMERATE,
             TEST_SEGMENT_WALKER,
@@ -1414,18 +1368,11 @@ public class QueryResourceTest
             new DefaultQueryConfig(Map.of()),
             null
         ),
-        jsonMapper,
-        queryScheduler,
         authMapper,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(
-            jsonMapper,
-            smileMapper
-        )
+        queryScheduler,
+        ResponseContextConfig.newConfig(true),
+        smileMapper,
+        new ServerConfig()
     );
 
     final String queryString = "{\"queryType\":\"timeBoundary\", 
\"dataSource\":\"allow\","
@@ -1791,7 +1738,7 @@ public class QueryResourceTest
       }
     };
 
-    queryResource = new QueryResource(
+    queryResource = createQueryResource(
         new QueryLifecycleFactory(
             CONGLOMERATE,
             texasRanger,
@@ -1804,18 +1751,11 @@ public class QueryResourceTest
             new DefaultQueryConfig(Map.of()),
             null
         ),
-        jsonMapper,
-        scheduler,
         null,
-        new QueryResourceQueryResultPusherFactory(
-            jsonMapper,
-            ResponseContextConfig.newConfig(true),
-            DRUID_NODE
-        ),
-        new ResourceIOReaderWriterFactory(
-            jsonMapper,
-            smileMapper
-        )
+        scheduler,
+        ResponseContextConfig.newConfig(true),
+        smileMapper,
+        new ServerConfig()
     );
   }
 
@@ -1835,6 +1775,218 @@ public class QueryResourceTest
     });
   }
 
+  @Test
+  public void testBlocklistedQueryReturnsForbidden() throws IOException
+  {
+    expectPermissiveHappyPathAuth();
+
+    final QueryResource blockingQueryResource = 
createQueryResourceWithBlocklist(
+        new ServerConfig(),
+        new DefaultQueryBlocklistRule("block-mmx", 
ImmutableSet.of("mmx_metrics"), null, null)
+    );
+
+    final Response response = blockingQueryResource.doPost(
+        new 
ByteArrayInputStream(SIMPLE_TIMESERIES_QUERY.getBytes(StandardCharsets.UTF_8)),
+        null /*pretty*/,
+        testServletRequest
+    );
+
+    Assert.assertNotNull(response);
+    Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), 
response.getStatus());
+    
Assert.assertNotNull(response.getMetadata().getFirst(QueryResource.QUERY_ID_RESPONSE_HEADER));
+
+    MatcherAssert.assertThat(
+        ((ErrorResponse) response.getEntity()).getUnderlyingException(),
+        DruidExceptionMatcher.forbidden().expectMessageContains("blocked by 
rule[block-mmx]")
+    );
+
+    // Blocked queries are still recorded in metrics and the request log. 
FORBIDDEN maps to no query counter, the
+    // same as when the exception surfaces through QueryResultPusher.
+    Assert.assertEquals(0, blockingQueryResource.getFailedQueryCount());
+    Assert.assertEquals(0, blockingQueryResource.getInterruptedQueryCount());
+    Assert.assertEquals(1, testRequestLogger.getNativeQuerylogs().size());
+    final Map<String, Object> stats = 
testRequestLogger.getNativeQuerylogs().get(0).getQueryStats().getStats();
+    Assert.assertEquals(false, stats.get("success"));
+    // The blocklist throws mid-authorization, so the identity is only present 
if it was recorded before that point.
+    Assert.assertEquals(AUTHENTICATION_RESULT.getIdentity(), 
stats.get("identity"));
+    Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), 
stats.get(DruidMetrics.STATUS_CODE));
+  }
+
+  @Test
+  public void 
testBlocklistedQueryIsSanitizedByErrorResponseTransformStrategy() throws 
IOException
+  {
+    expectPermissiveHappyPathAuth();
+
+    // Always-transforming strategy, so the test does not depend on any 
particular strategy's persona rules.
+    final ErrorResponseTransformStrategy strategy = new 
ErrorResponseTransformStrategy()
+    {
+      @Override
+      public Optional<DruidException> maybeTransform(DruidException exception, 
Optional<String> errorId)
+      {
+        return Optional.of(
+            DruidException.forPersona(DruidException.Persona.USER)
+                          .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                          .build("sanitized[%s]", errorId.orElse(null))
+        );
+      }
+
+      @Override
+      public Function<String, String> getErrorMessageTransformFunction()
+      {
+        throw new UnsupportedOperationException();
+      }
+    };
+
+    final QueryResource blockingQueryResource = 
createQueryResourceWithBlocklist(
+        new ServerConfig(strategy),
+        new DefaultQueryBlocklistRule("block-mmx", 
ImmutableSet.of("mmx_metrics"), null, null)
+    );
+
+    final Response response = blockingQueryResource.doPost(
+        new 
ByteArrayInputStream(SIMPLE_TIMESERIES_QUERY.getBytes(StandardCharsets.UTF_8)),
+        null /*pretty*/,
+        testServletRequest
+    );
+
+    Assert.assertNotNull(response);
+    // The transformed exception's own category now drives the status code, 
not the original FORBIDDEN.
+    Assert.assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
response.getStatus());
+
+    final Object queryId = 
response.getMetadata().getFirst(QueryResource.QUERY_ID_RESPONSE_HEADER);
+    Assert.assertNotNull(queryId);
+    MatcherAssert.assertThat(
+        ((ErrorResponse) response.getEntity()).getUnderlyingException(),
+        new DruidExceptionMatcher(
+            DruidException.Persona.USER,
+            DruidException.Category.RUNTIME_FAILURE,
+            "general"
+        ).expectMessageIs(StringUtils.format("sanitized[%s]", queryId))
+    );
+
+    // Sanitization applies to the client response only; the request log keeps 
the original 403.
+    Assert.assertEquals(1, testRequestLogger.getNativeQuerylogs().size());
+    Assert.assertEquals(
+        Status.FORBIDDEN.getStatusCode(),
+        
testRequestLogger.getNativeQuerylogs().get(0).getQueryStats().getStats().get(DruidMetrics.STATUS_CODE)
+    );
+  }
+
+  @Test
+  public void testDruidExceptionFromAuthorizeIsCountedByCategory() throws 
IOException
+  {
+    expectPermissiveHappyPathAuth();
+
+    final QueryLifecycleFactory realFactory = createQueryLifecycleFactory();
+    final QueryLifecycleFactory failingFactory = 
Mockito.mock(QueryLifecycleFactory.class);
+    Mockito.when(failingFactory.factorize()).thenAnswer(invocation -> {
+      final QueryLifecycle lifecycle = Mockito.spy(realFactory.factorize());
+      // DEFENSIVE maps to the "failed" counter, unlike the FORBIDDEN thrown 
by the blocklist.
+      Mockito.doThrow(DruidException.defensive("oh no"))
+             .when(lifecycle)
+             .authorize(ArgumentMatchers.any(HttpServletRequest.class));
+      return lifecycle;
+    });
+
+    final QueryResource failingQueryResource = 
createQueryResource(failingFactory);
+
+    final Response response = failingQueryResource.doPost(
+        new 
ByteArrayInputStream(SIMPLE_TIMESERIES_QUERY.getBytes(StandardCharsets.UTF_8)),
+        null /*pretty*/,
+        testServletRequest
+    );
+
+    Assert.assertNotNull(response);
+    Assert.assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
response.getStatus());
+    Assert.assertEquals(1, failingQueryResource.getFailedQueryCount());
+    Assert.assertEquals(0, failingQueryResource.getInterruptedQueryCount());
+  }
+
+  @Test
+  public void testQueryExceptionFromAuthorizeIsRecorded() throws IOException
+  {
+    expectPermissiveHappyPathAuth();
+
+    final QueryLifecycleFactory realFactory = createQueryLifecycleFactory();
+    final QueryLifecycleFactory failingFactory = 
Mockito.mock(QueryLifecycleFactory.class);
+    Mockito.when(failingFactory.factorize()).thenAnswer(invocation -> {
+      final QueryLifecycle lifecycle = Mockito.spy(realFactory.factorize());
+      
Mockito.doThrow(QueryCapacityExceededException.withErrorMessageAndResolvedHost("too
 busy"))
+             .when(lifecycle)
+             .authorize(ArgumentMatchers.any(HttpServletRequest.class));
+      return lifecycle;
+    });
+
+    final QueryResource failingQueryResource = 
createQueryResource(failingFactory);
+
+    final Response response = failingQueryResource.doPost(
+        new 
ByteArrayInputStream(SIMPLE_TIMESERIES_QUERY.getBytes(StandardCharsets.UTF_8)),
+        null /*pretty*/,
+        testServletRequest
+    );
+
+    // A QueryException keeps its own status and its legacy response body.
+    Assert.assertNotNull(response);
+    Assert.assertEquals(429, response.getStatus());
+    MatcherAssert.assertThat(
+        StringUtils.fromUtf8((byte[]) response.getEntity()),
+        CoreMatchers.containsString("too busy")
+    );
+
+    // It now shares the DruidException path, so the failure is recorded 
server-side and not just returned.
+    Assert.assertEquals(1, failingQueryResource.getFailedQueryCount());
+    Assert.assertEquals(1, testRequestLogger.getNativeQuerylogs().size());
+    final Map<String, Object> stats = 
testRequestLogger.getNativeQuerylogs().get(0).getQueryStats().getStats();
+    Assert.assertEquals(false, stats.get("success"));
+    Assert.assertEquals(429, stats.get(DruidMetrics.STATUS_CODE));
+  }
+
+  @Test
+  public void testNonBlocklistedQueryIsNotAffectedByBlocklist() throws 
IOException
+  {
+    expectPermissiveHappyPathAuth();
+
+    final QueryResource blockingQueryResource = 
createQueryResourceWithBlocklist(
+        new ServerConfig(),
+        new DefaultQueryBlocklistRule("block-other", 
ImmutableSet.of("some_other_datasource"), null, null)
+    );
+
+    final MockHttpServletResponse response = expectAsyncRequestFlow(
+        testServletRequest,
+        SIMPLE_TIMESERIES_QUERY.getBytes(StandardCharsets.UTF_8),
+        blockingQueryResource
+    );
+
+    Assert.assertEquals(Status.OK.getStatusCode(), response.getStatus());
+  }
+
+  private QueryResource createQueryResourceWithBlocklist(ServerConfig 
serverConfig, QueryBlocklistRule... rules)
+  {
+    final BrokerViewOfBrokerConfig brokerViewOfBrokerConfig = 
Mockito.mock(BrokerViewOfBrokerConfig.class);
+    Mockito.when(brokerViewOfBrokerConfig.getDynamicConfig()).thenReturn(
+        new 
BrokerDynamicConfig.Builder().withQueryBlocklist(Arrays.asList(rules)).build()
+    );
+
+    return createQueryResource(
+        new QueryLifecycleFactory(
+            CONGLOMERATE,
+            TEST_SEGMENT_WALKER,
+            new DefaultGenericQueryMetricsFactory(),
+            emitter,
+            testRequestLogger,
+            new AuthConfig(),
+            NoopPolicyEnforcer.instance(),
+            AuthTestUtils.TEST_AUTHORIZER_MAPPER,
+            new DefaultQueryConfig(Map.of()),
+            brokerViewOfBrokerConfig
+        ),
+        null,
+        queryScheduler,
+        ResponseContextConfig.newConfig(true),
+        smileMapper,
+        serverConfig
+    );
+  }
+
   private void expectPermissiveHappyPathAuth()
   {
     testServletRequest.setAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT, 
AUTHENTICATION_RESULT);
diff --git a/sql/src/main/java/org/apache/druid/sql/avatica/ErrorHandler.java 
b/sql/src/main/java/org/apache/druid/sql/avatica/ErrorHandler.java
index 8e2927279ee..32a0369ce17 100644
--- a/sql/src/main/java/org/apache/druid/sql/avatica/ErrorHandler.java
+++ b/sql/src/main/java/org/apache/druid/sql/avatica/ErrorHandler.java
@@ -87,9 +87,10 @@ class ErrorHandler
       return new 
RuntimeException(errorResponseTransformStrategy.transformIfNeeded((SanitizableException)
 error.getCause()));
     }
     if (error instanceof DruidException) {
+      final DruidException druidError = (DruidException) error;
       String errorId = UUID.randomUUID().toString();
-      Optional<Exception> transformedException = 
errorResponseTransformStrategy.maybeTransform(
-          (DruidException) error,
+      Optional<DruidException> transformedException = 
errorResponseTransformStrategy.maybeTransform(
+          druidError,
           Optional.of(errorId)
       );
 
@@ -97,7 +98,7 @@ class ErrorHandler
         // Log the exception here itself, since the error has been transformed.
         log.error(error, StringUtils.format("External Error ID: [%s]", 
errorId));
       }
-      QueryInterruptedException wrappedError = 
QueryInterruptedException.wrapIfNeeded(transformedException.orElse((Exception) 
error));
+      QueryInterruptedException wrappedError = 
QueryInterruptedException.wrapIfNeeded(transformedException.orElse(druidError));
       return (QueryException) 
errorResponseTransformStrategy.transformIfNeeded(wrappedError);
     }
     QueryInterruptedException wrappedError = 
QueryInterruptedException.wrapIfNeeded(error);
diff --git a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java 
b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java
index 4ca78c2774a..41b9f6cb84f 100644
--- a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java
+++ b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java
@@ -371,32 +371,36 @@ public class SqlResource
       final ErrorResponseTransformStrategy strategy
   )
   {
+    final String sqlQueryId = 
queryContext.getString(QueryContexts.CTX_SQL_QUERY_ID);
+    final String errorId = sqlQueryId == null ? UUID.randomUUID().toString() : 
sqlQueryId;
+
+    final DruidException druidException;
+    final Map<String, String> headers;
     if (e instanceof DruidException) {
-      final String sqlQueryId = 
queryContext.getString(QueryContexts.CTX_SQL_QUERY_ID);
-      String errorId = sqlQueryId == null ? UUID.randomUUID().toString() : 
sqlQueryId;
-      Optional<Exception> transformed = 
strategy.maybeTransform((DruidException) e, Optional.of(errorId));
-      if (transformed.isPresent()) {
-        // Log the exception here itself, since the error has been transformed.
-        log.error(e, StringUtils.format("External Error ID: [%s]", errorId));
-      }
-      return QueryResultPusher.handleDruidExceptionBeforeResponseStarted(
-          (DruidException) transformed.orElse(e),
-          MediaType.APPLICATION_JSON_TYPE,
-          sqlQueryId != null
-          ? ImmutableMap.<String, String>builder()
-                        .put(QueryResource.QUERY_ID_RESPONSE_HEADER, 
sqlQueryId)
-                        .put(SQL_QUERY_ID_RESPONSE_HEADER, sqlQueryId)
-                        .build()
-          : Collections.emptyMap()
-      );
+      druidException = (DruidException) e;
+      headers = sqlQueryId != null
+                ? ImmutableMap.<String, String>builder()
+                              .put(QueryResource.QUERY_ID_RESPONSE_HEADER, 
sqlQueryId)
+                              .put(SQL_QUERY_ID_RESPONSE_HEADER, sqlQueryId)
+                              .build()
+                : Collections.emptyMap();
     } else {
-      return QueryResultPusher.handleDruidExceptionBeforeResponseStarted(
-          DruidException.forPersona(DruidException.Persona.OPERATOR)
-                        .ofCategory(DruidException.Category.RUNTIME_FAILURE)
-                        .build(e, "Cannot handle query"),
-          MediaType.APPLICATION_JSON_TYPE,
-          Collections.emptyMap()
-      );
+      druidException = 
DruidException.forPersona(DruidException.Persona.OPERATOR)
+                                     
.ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                                     .build(e, "Cannot handle query");
+      headers = Collections.emptyMap();
     }
+
+    final Optional<DruidException> transformed = 
strategy.maybeTransform(druidException, Optional.of(errorId));
+    if (transformed.isPresent()) {
+      // Nothing else logs this failure, and the client is only given the 
error id from here on.
+      log.error(e, StringUtils.format("External Error ID: [%s]", errorId));
+    }
+
+    return QueryResultPusher.handleDruidExceptionBeforeResponseStarted(
+        transformed.orElse(druidException),
+        MediaType.APPLICATION_JSON_TYPE,
+        headers
+    );
   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to