This is an automated email from the ASF dual-hosted git repository.
maytasm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new a04b08e Add new config to filter internal Druid-related messages from
Query API response (#11711)
a04b08e is described below
commit a04b08e45ce82c6c27eca94b735e624349846bc5
Author: Maytas Monsereenusorn <[email protected]>
AuthorDate: Wed Sep 29 12:55:49 2021 +0700
Add new config to filter internal Druid-related messages from Query API
response (#11711)
* add impl
* add impl
* add tests
* add unit test
* fix checkstyle
* address comments
* fix checkstyle
* fix checkstyle
* fix checkstyle
* fix checkstyle
* fix checkstyle
* address comments
* address comments
* address comments
* fix test
* fix test
* fix test
* fix test
* fix test
* change config name
* change config name
* change config name
* address comments
* address comments
* address comments
* address comments
* address comments
* address comments
* fix compile
* fix compile
* change config
* add more tests
* fix IT
---
...AllowedRegexErrorResponseTransformStrategy.java | 80 +++++++
.../exception/ErrorResponseTransformStrategy.java | 50 ++++
.../NoErrorResponseTransformStrategy.java | 57 +++++
.../common/exception/SanitizableException.java | 37 +++
.../org/apache/druid/query/QueryException.java | 11 +-
...wedRegexErrorResponseTransformStrategyTest.java | 70 ++++++
.../NoErrorResponseTransformStrategyTest.java | 31 +--
.../org/apache/druid/query/QueryExceptionTest.java | 73 ++++++
.../org/apache/druid/tests/security/ITTLSTest.java | 2 +-
.../druid/server/initialization/ServerConfig.java | 39 ++-
.../jetty/CliIndexerServerModule.java | 4 +-
.../initialization/jetty/JettyServerModule.java | 29 +++
.../druid/server/security/ForbiddenException.java | 21 +-
.../druid/initialization/ServerConfigTest.java | 6 +-
.../druid/server/initialization/BaseJettyTest.java | 2 +-
.../druid/server/initialization/JettyTest.java | 7 +
.../JettyWithResponseFilterEnabledTest.java} | 32 +--
.../server/security/ForbiddenExceptionTest.java | 64 +++++
.../druid/server/AsyncQueryForwardingServlet.java | 28 ++-
.../server/AsyncQueryForwardingServletTest.java | 262 ++++++++++++++++++++-
.../org/apache/druid/sql/http/SqlResource.java | 17 +-
.../org/apache/druid/sql/http/SqlResourceTest.java | 93 ++++++--
22 files changed, 921 insertions(+), 94 deletions(-)
diff --git
a/core/src/main/java/org/apache/druid/common/exception/AllowedRegexErrorResponseTransformStrategy.java
b/core/src/main/java/org/apache/druid/common/exception/AllowedRegexErrorResponseTransformStrategy.java
new file mode 100644
index 0000000..dde76bd
--- /dev/null
+++
b/core/src/main/java/org/apache/druid/common/exception/AllowedRegexErrorResponseTransformStrategy.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.common.exception;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class AllowedRegexErrorResponseTransformStrategy implements
ErrorResponseTransformStrategy
+{
+ @JsonProperty
+ private final List<String> allowedRegexString;
+
+ private final List<Pattern> allowedRegexPattern;
+
+ @JsonCreator
+ public AllowedRegexErrorResponseTransformStrategy(
+ @JsonProperty("allowedRegex") List<String> allowedRegexString
+ )
+ {
+ this.allowedRegexString = allowedRegexString;
+ this.allowedRegexPattern = allowedRegexString == null
+ ? ImmutableList.of()
+ :
allowedRegexString.stream().map(Pattern::compile).collect(Collectors.toList());
+ }
+
+ @Override
+ public Function<String, String> getErrorMessageTransformFunction()
+ {
+ return (String errorMessage) -> {
+ if (allowedRegexPattern.stream().anyMatch(pattern ->
pattern.matcher(errorMessage).matches())) {
+ return errorMessage;
+ } else {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ AllowedRegexErrorResponseTransformStrategy that =
(AllowedRegexErrorResponseTransformStrategy) o;
+ return Objects.equals(allowedRegexString, that.allowedRegexString);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(allowedRegexString);
+ }
+}
diff --git
a/core/src/main/java/org/apache/druid/common/exception/ErrorResponseTransformStrategy.java
b/core/src/main/java/org/apache/druid/common/exception/ErrorResponseTransformStrategy.java
new file mode 100644
index 0000000..d7a2bbd
--- /dev/null
+++
b/core/src/main/java/org/apache/druid/common/exception/ErrorResponseTransformStrategy.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.common.exception;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import javax.validation.constraints.NotNull;
+import java.util.function.Function;
+
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "strategy", defaultImpl =
NoErrorResponseTransformStrategy.class)
+@JsonSubTypes(value = {
+ @JsonSubTypes.Type(name = "none", value =
NoErrorResponseTransformStrategy.class),
+ @JsonSubTypes.Type(name = "allowedRegex", value =
AllowedRegexErrorResponseTransformStrategy.class)
+})
+public interface ErrorResponseTransformStrategy
+{
+ /**
+ * For a given {@link SanitizableException} apply the transformation
strategy and return the sanitized Exception
+ * if the transformation stategy was applied.
+ */
+ default Exception transformIfNeeded(SanitizableException exception)
+ {
+ return exception.sanitize(getErrorMessageTransformFunction());
+ }
+
+ /**
+ * 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.
+ */
+ @NotNull
+ Function<String, String> getErrorMessageTransformFunction();
+}
diff --git
a/core/src/main/java/org/apache/druid/common/exception/NoErrorResponseTransformStrategy.java
b/core/src/main/java/org/apache/druid/common/exception/NoErrorResponseTransformStrategy.java
new file mode 100644
index 0000000..95ebb8b
--- /dev/null
+++
b/core/src/main/java/org/apache/druid/common/exception/NoErrorResponseTransformStrategy.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.common.exception;
+
+import java.util.function.Function;
+
+/**
+ * Error response transform strategy that does nothing and simply return the
same Exception back without any change
+ */
+public class NoErrorResponseTransformStrategy implements
ErrorResponseTransformStrategy
+{
+ public static final NoErrorResponseTransformStrategy INSTANCE = new
NoErrorResponseTransformStrategy();
+
+ @Override
+ public Exception transformIfNeeded(SanitizableException exception)
+ {
+ return (Exception) exception;
+ }
+
+ @Override
+ public Function<String, String> getErrorMessageTransformFunction()
+ {
+ return (String errorMessage) -> errorMessage;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) {
+ return true;
+ }
+ return !(o == null || getClass() != o.getClass());
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return NoErrorResponseTransformStrategy.class.hashCode();
+ }
+}
diff --git
a/core/src/main/java/org/apache/druid/common/exception/SanitizableException.java
b/core/src/main/java/org/apache/druid/common/exception/SanitizableException.java
new file mode 100644
index 0000000..2ac15e0
--- /dev/null
+++
b/core/src/main/java/org/apache/druid/common/exception/SanitizableException.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.common.exception;
+
+import java.util.function.Function;
+
+public interface SanitizableException
+{
+ /**
+ * Apply the function for transforming the error message then return new
Exception with sanitized fields and transformed message.
+ * The {@param errorMessageTransformFunction} is only intended to be use to
transform the error message
+ * String of the Exception as only the error message String is common to all
Exception classes.
+ * For other fields (which may be unique to each particular Exception
class), each implementation of this method can
+ * decide for itself how to sanitized those fields (i.e. leaving unchanged,
changing to null, changing to a fixed String, etc.).
+ * Note that this method returns a new Exception of the same type since
Exception error message is immutable.
+ */
+ Exception sanitize(
+ Function<String, String> errorMessageTransformFunction
+ );
+}
diff --git a/core/src/main/java/org/apache/druid/query/QueryException.java
b/core/src/main/java/org/apache/druid/query/QueryException.java
index 108e9f7..10267b5 100644
--- a/core/src/main/java/org/apache/druid/query/QueryException.java
+++ b/core/src/main/java/org/apache/druid/query/QueryException.java
@@ -22,16 +22,19 @@ package org.apache.druid.query;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
+import org.apache.druid.common.exception.SanitizableException;
import javax.annotation.Nullable;
+import javax.validation.constraints.NotNull;
import java.net.InetAddress;
+import java.util.function.Function;
/**
* Base serializable error response
*
* QueryResource and SqlResource are expected to emit the JSON form of this
object when errors happen.
*/
-public class QueryException extends RuntimeException
+public class QueryException extends RuntimeException implements
SanitizableException
{
private final String errorCode;
private final String errorClass;
@@ -96,4 +99,10 @@ public class QueryException extends RuntimeException
return null;
}
}
+
+ @Override
+ public QueryException sanitize(@NotNull Function<String, String>
errorMessageTransformFunction)
+ {
+ return new QueryException(errorCode,
errorMessageTransformFunction.apply(getMessage()), null, null);
+ }
}
diff --git
a/core/src/test/java/org/apache/druid/common/exception/AllowedRegexErrorResponseTransformStrategyTest.java
b/core/src/test/java/org/apache/druid/common/exception/AllowedRegexErrorResponseTransformStrategyTest.java
new file mode 100644
index 0000000..9ababc8
--- /dev/null
+++
b/core/src/test/java/org/apache/druid/common/exception/AllowedRegexErrorResponseTransformStrategyTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.common.exception;
+
+import com.google.common.collect.ImmutableList;
+import nl.jqno.equalsverifier.EqualsVerifier;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AllowedRegexErrorResponseTransformStrategyTest
+{
+ @Test
+ public void
testGetErrorMessageTransformFunctionWithMatchingAllowedRegexFilter()
+ {
+ AllowedRegexErrorResponseTransformStrategy allowedRegex = new
AllowedRegexErrorResponseTransformStrategy(
+ ImmutableList.of("acbd", "test .*")
+ );
+ String message = "test message 123";
+ String result =
allowedRegex.getErrorMessageTransformFunction().apply(message);
+ Assert.assertEquals(message, result);
+ }
+
+ @Test
+ public void
testGetErrorMessageTransformFunctionWithNoMatchingAllowedRegexFilter()
+ {
+ AllowedRegexErrorResponseTransformStrategy allowedRegex = new
AllowedRegexErrorResponseTransformStrategy(
+ ImmutableList.of("acbd", "qwer")
+ );
+ String message = "test message 123";
+ String result =
allowedRegex.getErrorMessageTransformFunction().apply(message);
+ Assert.assertNull(result);
+ }
+
+ @Test
+ public void testGetErrorMessageTransformFunctionWithEmptyAllowedRegexFilter()
+ {
+ AllowedRegexErrorResponseTransformStrategy allowedRegex = new
AllowedRegexErrorResponseTransformStrategy(
+ ImmutableList.of()
+ );
+ String message = "test message 123";
+ String result =
allowedRegex.getErrorMessageTransformFunction().apply(message);
+ Assert.assertNull(result);
+ }
+
+ @Test
+ public void testEqualsAndHashCode()
+ {
+ EqualsVerifier.forClass(AllowedRegexErrorResponseTransformStrategy.class)
+ .withIgnoredFields("allowedRegexPattern")
+ .usingGetClass()
+ .verify();
+ }
+}
diff --git
a/server/src/main/java/org/apache/druid/server/security/ForbiddenException.java
b/core/src/test/java/org/apache/druid/common/exception/NoErrorResponseTransformStrategyTest.java
similarity index 56%
copy from
server/src/main/java/org/apache/druid/server/security/ForbiddenException.java
copy to
core/src/test/java/org/apache/druid/common/exception/NoErrorResponseTransformStrategyTest.java
index 6cf326c..8e0394c 100644
---
a/server/src/main/java/org/apache/druid/server/security/ForbiddenException.java
+++
b/core/src/test/java/org/apache/druid/common/exception/NoErrorResponseTransformStrategyTest.java
@@ -17,31 +17,18 @@
* under the License.
*/
-package org.apache.druid.server.security;
+package org.apache.druid.common.exception;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import nl.jqno.equalsverifier.EqualsVerifier;
+import org.junit.Test;
-/**
- * Throw this when a request is unauthorized and we want to send a 403
response back, Jersey exception mapper will
- * take care of sending the response.
- */
-public class ForbiddenException extends RuntimeException
+public class NoErrorResponseTransformStrategyTest
{
- public ForbiddenException()
- {
- super("Unauthorized.");
- }
-
- @JsonCreator
- public ForbiddenException(@JsonProperty("errorMessage") String msg)
- {
- super(msg);
- }
-
- @JsonProperty
- public String getErrorMessage()
+ @Test
+ public void testEqualsAndHashCode()
{
- return super.getMessage();
+ EqualsVerifier.forClass(NoErrorResponseTransformStrategy.class)
+ .usingGetClass()
+ .verify();
}
}
diff --git a/core/src/test/java/org/apache/druid/query/QueryExceptionTest.java
b/core/src/test/java/org/apache/druid/query/QueryExceptionTest.java
new file mode 100644
index 0000000..51ff763
--- /dev/null
+++ b/core/src/test/java/org/apache/druid/query/QueryExceptionTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.query;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.function.Function;
+
+@RunWith(MockitoJUnitRunner.class)
+public class QueryExceptionTest
+{
+ private static final String ERROR_CODE = "error code";
+ private static final String ERROR_CLASS = "error code";
+ private static final String HOST = "error code";
+ private static final String ERROR_MESSAGE_ORIGINAL = "aaaa";
+ private static final String ERROR_MESSAGE_TRANSFORMED = "bbbb";
+
+ @Mock
+ private Function<String, String> trasformFunction;
+
+ @Test
+ public void testSanitizeWithTransformFunctionReturningNull()
+ {
+
Mockito.when(trasformFunction.apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL))).thenReturn(null);
+ QueryException queryException = new QueryException(ERROR_CODE,
ERROR_MESSAGE_ORIGINAL, ERROR_CLASS, HOST);
+ QueryException actual = queryException.sanitize(trasformFunction);
+ Assert.assertNotNull(actual);
+ Assert.assertEquals(actual.getErrorCode(), ERROR_CODE);
+ Assert.assertNull(actual.getMessage());
+ Assert.assertNull(actual.getHost());
+ Assert.assertNull(actual.getErrorClass());
+
Mockito.verify(trasformFunction).apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL));
+ Mockito.verifyNoMoreInteractions(trasformFunction);
+ }
+
+ @Test
+ public void testSanitizeWithTransformFunctionReturningNewString()
+ {
+
Mockito.when(trasformFunction.apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL))).thenReturn(ERROR_MESSAGE_TRANSFORMED);
+ QueryException queryException = new QueryException(ERROR_CODE,
ERROR_MESSAGE_ORIGINAL, ERROR_CLASS, HOST);
+ QueryException actual = queryException.sanitize(trasformFunction);
+ Assert.assertNotNull(actual);
+ Assert.assertEquals(actual.getErrorCode(), ERROR_CODE);
+ Assert.assertEquals(actual.getMessage(), ERROR_MESSAGE_TRANSFORMED);
+ Assert.assertNull(actual.getHost());
+ Assert.assertNull(actual.getErrorClass());
+
Mockito.verify(trasformFunction).apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL));
+ Mockito.verifyNoMoreInteractions(trasformFunction);
+ }
+}
diff --git
a/integration-tests/src/test/java/org/apache/druid/tests/security/ITTLSTest.java
b/integration-tests/src/test/java/org/apache/druid/tests/security/ITTLSTest.java
index f889a2b..09167c0 100644
---
a/integration-tests/src/test/java/org/apache/druid/tests/security/ITTLSTest.java
+++
b/integration-tests/src/test/java/org/apache/druid/tests/security/ITTLSTest.java
@@ -258,7 +258,7 @@ public class ITTLSTest
config.getCustomCertCheckRouterTLSUrl() + "/druid/v2",
"Custom cert check",
ISE.class,
- "Error while making request to url[https://127.0.0.1:9091/druid/v2]
status[400 Bad Request] content[{\"error\":\"No content to map due to
end-of-input",
+ "Error while making request to url[https://127.0.0.1:9091/druid/v2]
status[400 Bad Request] content[{\"error\":\"Unknown
exception\",\"errorMessage\":\"No content to map due to end-of-input",
true
);
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 ac9576b..646c0e5 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
@@ -21,6 +21,8 @@ package org.apache.druid.server.initialization;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.ImmutableList;
+import org.apache.druid.common.exception.ErrorResponseTransformStrategy;
+import org.apache.druid.common.exception.NoErrorResponseTransformStrategy;
import org.apache.druid.java.util.common.HumanReadableBytes;
import org.apache.druid.java.util.common.HumanReadableBytesRange;
import org.apache.druid.utils.JvmUtils;
@@ -61,7 +63,9 @@ public class ServerConfig
int inflateBufferSize,
int compressionLevel,
boolean enableForwardedRequestCustomizer,
- @NotNull List<String> allowedHttpMethods
+ @NotNull List<String> allowedHttpMethods,
+ boolean showDetailedJettyErrors,
+ ErrorResponseTransformStrategy errorResponseTransformStrategy
)
{
this.numThreads = numThreads;
@@ -79,6 +83,8 @@ public class ServerConfig
this.compressionLevel = compressionLevel;
this.enableForwardedRequestCustomizer = enableForwardedRequestCustomizer;
this.allowedHttpMethods = allowedHttpMethods;
+ this.showDetailedJettyErrors = showDetailedJettyErrors;
+ this.errorResponseTransformStrategy = errorResponseTransformStrategy;
}
public ServerConfig()
@@ -145,6 +151,13 @@ public class ServerConfig
@NotNull
private List<String> allowedHttpMethods = ImmutableList.of();
+ @JsonProperty("errorResponseTransform")
+ @NotNull
+ private ErrorResponseTransformStrategy errorResponseTransformStrategy =
NoErrorResponseTransformStrategy.INSTANCE;
+
+ @JsonProperty
+ private boolean showDetailedJettyErrors = true;
+
public int getNumThreads()
{
return numThreads;
@@ -215,6 +228,16 @@ public class ServerConfig
return enableForwardedRequestCustomizer;
}
+ public boolean isShowDetailedJettyErrors()
+ {
+ return showDetailedJettyErrors;
+ }
+
+ public ErrorResponseTransformStrategy getErrorResponseTransformStrategy()
+ {
+ return errorResponseTransformStrategy;
+ }
+
@NotNull
public List<String> getAllowedHttpMethods()
{
@@ -235,17 +258,19 @@ public class ServerConfig
queueSize == that.queueSize &&
enableRequestLimit == that.enableRequestLimit &&
defaultQueryTimeout == that.defaultQueryTimeout &&
- maxScatterGatherBytes.equals(that.maxScatterGatherBytes) &&
maxSubqueryRows == that.maxSubqueryRows &&
maxQueryTimeout == that.maxQueryTimeout &&
maxRequestHeaderSize == that.maxRequestHeaderSize &&
inflateBufferSize == that.inflateBufferSize &&
compressionLevel == that.compressionLevel &&
enableForwardedRequestCustomizer ==
that.enableForwardedRequestCustomizer &&
+ showDetailedJettyErrors == that.showDetailedJettyErrors &&
maxIdleTime.equals(that.maxIdleTime) &&
+ maxScatterGatherBytes.equals(that.maxScatterGatherBytes) &&
gracefulShutdownTimeout.equals(that.gracefulShutdownTimeout) &&
unannouncePropagationDelay.equals(that.unannouncePropagationDelay)
&&
- allowedHttpMethods.equals(that.allowedHttpMethods);
+ allowedHttpMethods.equals(that.allowedHttpMethods) &&
+
errorResponseTransformStrategy.equals(that.errorResponseTransformStrategy);
}
@Override
@@ -266,7 +291,9 @@ public class ServerConfig
inflateBufferSize,
compressionLevel,
enableForwardedRequestCustomizer,
- allowedHttpMethods
+ allowedHttpMethods,
+ errorResponseTransformStrategy,
+ showDetailedJettyErrors
);
}
@@ -288,7 +315,9 @@ public class ServerConfig
", inflateBufferSize=" + inflateBufferSize +
", compressionLevel=" + compressionLevel +
", enableForwardedRequestCustomizer=" +
enableForwardedRequestCustomizer +
- ", allowedMethods=" + allowedHttpMethods +
+ ", allowedHttpMethods=" + allowedHttpMethods +
+ ", errorResponseTransformStrategy=" +
errorResponseTransformStrategy +
+ ", showDetailedJettyErrors=" + showDetailedJettyErrors +
'}';
}
diff --git
a/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
b/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
index d8f6bd1..06561b6 100644
---
a/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
+++
b/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
@@ -160,7 +160,9 @@ public class CliIndexerServerModule implements Module
oldConfig.getInflateBufferSize(),
oldConfig.getCompressionLevel(),
oldConfig.isEnableForwardedRequestCustomizer(),
- oldConfig.getAllowedHttpMethods()
+ oldConfig.getAllowedHttpMethods(),
+ oldConfig.isShowDetailedJettyErrors(),
+ oldConfig.getErrorResponseTransformStrategy()
);
}
}
diff --git
a/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java
b/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java
index 351a4c0..41a00a2 100644
---
a/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java
+++
b/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java
@@ -68,10 +68,12 @@ import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
@@ -82,6 +84,11 @@ import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedTrustManager;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
import java.security.KeyStore;
import java.security.cert.CRL;
import java.util.ArrayList;
@@ -466,6 +473,28 @@ public class JettyServerModule extends JerseyServletModule
Lifecycle.Stage.SERVER
);
+ if (!config.isShowDetailedJettyErrors()) {
+ server.setErrorHandler(new ErrorHandler() {
+ @Override
+ public boolean isShowServlet()
+ {
+ return false;
+ }
+
+ @Override
+ public void handle(
+ String target,
+ Request baseRequest,
+ HttpServletRequest request,
+ HttpServletResponse response
+ ) throws IOException, ServletException
+ {
+ request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, null);
+ super.handle(target, baseRequest, request, response);
+ }
+ });
+ }
+
return server;
}
diff --git
a/server/src/main/java/org/apache/druid/server/security/ForbiddenException.java
b/server/src/main/java/org/apache/druid/server/security/ForbiddenException.java
index 6cf326c..7de37d6 100644
---
a/server/src/main/java/org/apache/druid/server/security/ForbiddenException.java
+++
b/server/src/main/java/org/apache/druid/server/security/ForbiddenException.java
@@ -21,16 +21,22 @@ package org.apache.druid.server.security;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Strings;
+import org.apache.druid.common.exception.SanitizableException;
+
+import java.util.function.Function;
/**
* Throw this when a request is unauthorized and we want to send a 403
response back, Jersey exception mapper will
* take care of sending the response.
*/
-public class ForbiddenException extends RuntimeException
+public class ForbiddenException extends RuntimeException implements
SanitizableException
{
+ static final String DEFAULT_ERROR_MESSAGE = "Unauthorized.";
+
public ForbiddenException()
{
- super("Unauthorized.");
+ super(DEFAULT_ERROR_MESSAGE);
}
@JsonCreator
@@ -44,4 +50,15 @@ public class ForbiddenException extends RuntimeException
{
return super.getMessage();
}
+
+ @Override
+ public ForbiddenException sanitize(Function<String, String>
errorMessageTransformFunction)
+ {
+ String transformedErrorMessage =
errorMessageTransformFunction.apply(getMessage());
+ if (Strings.isNullOrEmpty(transformedErrorMessage)) {
+ return new ForbiddenException();
+ } else {
+ return new ForbiddenException(transformedErrorMessage);
+ }
+ }
}
diff --git
a/server/src/test/java/org/apache/druid/initialization/ServerConfigTest.java
b/server/src/test/java/org/apache/druid/initialization/ServerConfigTest.java
index 4c8c908..e542ca8 100644
--- a/server/src/test/java/org/apache/druid/initialization/ServerConfigTest.java
+++ b/server/src/test/java/org/apache/druid/initialization/ServerConfigTest.java
@@ -22,6 +22,7 @@ package org.apache.druid.initialization;
import com.google.common.collect.ImmutableList;
import nl.jqno.equalsverifier.EqualsVerifier;
import nl.jqno.equalsverifier.Warning;
+import
org.apache.druid.common.exception.AllowedRegexErrorResponseTransformStrategy;
import org.apache.druid.jackson.DefaultObjectMapper;
import org.apache.druid.server.initialization.ServerConfig;
import org.junit.Assert;
@@ -57,13 +58,16 @@ public class ServerConfigTest
defaultConfig.getInflateBufferSize(),
defaultConfig.getCompressionLevel(),
true,
- ImmutableList.of(HttpMethod.OPTIONS)
+ ImmutableList.of(HttpMethod.OPTIONS),
+ true,
+ new AllowedRegexErrorResponseTransformStrategy(ImmutableList.of(".*"))
);
String modifiedConfigJson =
OBJECT_MAPPER.writeValueAsString(modifiedConfig);
ServerConfig modifiedConfig2 = OBJECT_MAPPER.readValue(modifiedConfigJson,
ServerConfig.class);
Assert.assertEquals(modifiedConfig, modifiedConfig2);
Assert.assertEquals(999, modifiedConfig2.getNumThreads());
Assert.assertEquals(888, modifiedConfig2.getQueueSize());
+ Assert.assertTrue(modifiedConfig2.getErrorResponseTransformStrategy()
instanceof AllowedRegexErrorResponseTransformStrategy);
Assert.assertTrue(modifiedConfig2.isEnableForwardedRequestCustomizer());
Assert.assertEquals(1, modifiedConfig2.getAllowedHttpMethods().size());
Assert.assertTrue(modifiedConfig2.getAllowedHttpMethods().contains(HttpMethod.OPTIONS));
diff --git
a/server/src/test/java/org/apache/druid/server/initialization/BaseJettyTest.java
b/server/src/test/java/org/apache/druid/server/initialization/BaseJettyTest.java
index 6e555a7..caf0900 100644
---
a/server/src/test/java/org/apache/druid/server/initialization/BaseJettyTest.java
+++
b/server/src/test/java/org/apache/druid/server/initialization/BaseJettyTest.java
@@ -76,7 +76,7 @@ public abstract class BaseJettyTest
protected int port = -1;
protected int tlsPort = -1;
- public static void setProperties()
+ protected void setProperties()
{
System.setProperty("druid.server.http.numThreads", "20");
System.setProperty("druid.server.http.maxIdleTime", "PT1S");
diff --git
a/server/src/test/java/org/apache/druid/server/initialization/JettyTest.java
b/server/src/test/java/org/apache/druid/server/initialization/JettyTest.java
index 4f2e90f..59c84b9 100644
--- a/server/src/test/java/org/apache/druid/server/initialization/JettyTest.java
+++ b/server/src/test/java/org/apache/druid/server/initialization/JettyTest.java
@@ -554,6 +554,13 @@ public class JettyTest extends BaseJettyTest
Assert.assertTrue(endpointIdentificationAlgorithm == null ||
endpointIdentificationAlgorithm.isEmpty());
}
+ @Test
+ public void testJettyErrorHandlerWithFilter()
+ {
+ // Response filter is disabled by default hence we show servlet information
+ Assert.assertTrue(server.getErrorHandler().isShowServlet());
+ }
+
private void waitForJettyServerModuleActiveConnectionsZero(JettyServerModule
jsm) throws InterruptedException
{
// it can take a bit to close the connection, so maybe sleep for a while
and hope it closes
diff --git
a/server/src/main/java/org/apache/druid/server/security/ForbiddenException.java
b/server/src/test/java/org/apache/druid/server/initialization/JettyWithResponseFilterEnabledTest.java
similarity index 57%
copy from
server/src/main/java/org/apache/druid/server/security/ForbiddenException.java
copy to
server/src/test/java/org/apache/druid/server/initialization/JettyWithResponseFilterEnabledTest.java
index 6cf326c..b788e71 100644
---
a/server/src/main/java/org/apache/druid/server/security/ForbiddenException.java
+++
b/server/src/test/java/org/apache/druid/server/initialization/JettyWithResponseFilterEnabledTest.java
@@ -17,31 +17,25 @@
* under the License.
*/
-package org.apache.druid.server.security;
+package org.apache.druid.server.initialization;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import org.junit.Assert;
+import org.junit.Test;
-/**
- * Throw this when a request is unauthorized and we want to send a 403
response back, Jersey exception mapper will
- * take care of sending the response.
- */
-public class ForbiddenException extends RuntimeException
+public class JettyWithResponseFilterEnabledTest extends JettyTest
{
- public ForbiddenException()
- {
- super("Unauthorized.");
- }
-
- @JsonCreator
- public ForbiddenException(@JsonProperty("errorMessage") String msg)
+ @Override
+ public void setProperties()
{
- super(msg);
+ System.setProperty("druid.server.http.showDetailedJettyErrors", "false");
+ super.setProperties();
}
- @JsonProperty
- public String getErrorMessage()
+ @Test
+ @Override
+ public void testJettyErrorHandlerWithFilter()
{
- return super.getMessage();
+ // Response filter is enabled by config hence we do not show servlet
information
+ Assert.assertFalse(server.getErrorHandler().isShowServlet());
}
}
diff --git
a/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java
b/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java
new file mode 100644
index 0000000..01179e6
--- /dev/null
+++
b/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.server.security;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.function.Function;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ForbiddenExceptionTest
+{
+ private static final String ERROR_MESSAGE_ORIGINAL = "aaaa";
+ private static final String ERROR_MESSAGE_TRANSFORMED = "bbbb";
+
+ @Mock
+ private Function<String, String> trasformFunction;
+
+ @Test
+ public void testSanitizeWithTransformFunctionReturningNull()
+ {
+
Mockito.when(trasformFunction.apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL))).thenReturn(null);
+ ForbiddenException forbiddenException = new
ForbiddenException(ERROR_MESSAGE_ORIGINAL);
+ ForbiddenException actual = forbiddenException.sanitize(trasformFunction);
+ Assert.assertNotNull(actual);
+ Assert.assertEquals(actual.getMessage(),
ForbiddenException.DEFAULT_ERROR_MESSAGE);
+
Mockito.verify(trasformFunction).apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL));
+ Mockito.verifyNoMoreInteractions(trasformFunction);
+ }
+
+ @Test
+ public void testSanitizeWithTransformFunctionReturningNewString()
+ {
+
Mockito.when(trasformFunction.apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL))).thenReturn(ERROR_MESSAGE_TRANSFORMED);
+ ForbiddenException forbiddenException = new
ForbiddenException(ERROR_MESSAGE_ORIGINAL);
+ ForbiddenException actual = forbiddenException.sanitize(trasformFunction);
+ Assert.assertNotNull(actual);
+ Assert.assertEquals(actual.getMessage(), ERROR_MESSAGE_TRANSFORMED);
+
Mockito.verify(trasformFunction).apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL));
+ Mockito.verifyNoMoreInteractions(trasformFunction);
+ }
+}
diff --git
a/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
b/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
index 8365ba1..87c1c15 100644
---
a/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
+++
b/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
@@ -42,8 +42,10 @@ import
org.apache.druid.java.util.emitter.service.ServiceEmitter;
import org.apache.druid.query.DruidMetrics;
import org.apache.druid.query.GenericQueryMetricsFactory;
import org.apache.druid.query.Query;
+import org.apache.druid.query.QueryInterruptedException;
import org.apache.druid.query.QueryMetrics;
import org.apache.druid.query.QueryToolChestWarehouse;
+import org.apache.druid.server.initialization.ServerConfig;
import org.apache.druid.server.log.RequestLogger;
import org.apache.druid.server.metrics.QueryCountStatsProvider;
import org.apache.druid.server.router.QueryHostFinder;
@@ -102,17 +104,18 @@ public class AsyncQueryForwardingServlet extends
AsyncProxyServlet implements Qu
private final AtomicLong failedQueryCount = new AtomicLong();
private final AtomicLong interruptedQueryCount = new AtomicLong();
- private static void handleException(HttpServletResponse response,
ObjectMapper objectMapper, Exception exception)
+ @VisibleForTesting
+ void handleException(HttpServletResponse response, ObjectMapper
objectMapper, Exception exception)
throws IOException
{
+ QueryInterruptedException exceptionToReport =
QueryInterruptedException.wrapIfNeeded(exception);
+ LOG.warn(exceptionToReport, "Unexpected exception occurs");
if (!response.isCommitted()) {
- final String errorMessage = exception.getMessage() == null ? "null
exception" : exception.getMessage();
-
response.resetBuffer();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
objectMapper.writeValue(
response.getOutputStream(),
- ImmutableMap.of("error", errorMessage)
+
serverConfig.getErrorResponseTransformStrategy().transformIfNeeded(exceptionToReport)
);
}
response.flushBuffer();
@@ -129,6 +132,7 @@ public class AsyncQueryForwardingServlet extends
AsyncProxyServlet implements Qu
private final GenericQueryMetricsFactory queryMetricsFactory;
private final AuthenticatorMapper authenticatorMapper;
private final ProtobufTranslation protobufTranslation;
+ private final ServerConfig serverConfig;
private final boolean routeSqlByStrategy;
@@ -146,7 +150,8 @@ public class AsyncQueryForwardingServlet extends
AsyncProxyServlet implements Qu
RequestLogger requestLogger,
GenericQueryMetricsFactory queryMetricsFactory,
AuthenticatorMapper authenticatorMapper,
- Properties properties
+ Properties properties,
+ final ServerConfig serverConfig
)
{
this.warehouse = warehouse;
@@ -163,6 +168,7 @@ public class AsyncQueryForwardingServlet extends
AsyncProxyServlet implements Qu
this.routeSqlByStrategy = Boolean.parseBoolean(
properties.getProperty(PROPERTY_SQL_ENABLE,
PROPERTY_SQL_ENABLE_DEFAULT)
);
+ this.serverConfig = serverConfig;
}
@Override
@@ -320,7 +326,8 @@ public class AsyncQueryForwardingServlet extends
AsyncProxyServlet implements Qu
interruptedQueryCount.incrementAndGet();
}
- private void handleQueryParseException(
+ @VisibleForTesting
+ void handleQueryParseException(
HttpServletRequest request,
HttpServletResponse response,
ObjectMapper objectMapper,
@@ -328,11 +335,12 @@ public class AsyncQueryForwardingServlet extends
AsyncProxyServlet implements Qu
boolean isNativeQuery
) throws IOException
{
- LOG.warn(parseException, "Exception parsing query");
+ QueryInterruptedException exceptionToReport =
QueryInterruptedException.wrapIfNeeded(parseException);
+ LOG.warn(exceptionToReport, "Exception parsing query");
// Log the error message
- final String errorMessage = parseException.getMessage() == null
- ? "no error message" :
parseException.getMessage();
+ final String errorMessage = exceptionToReport.getMessage() == null
+ ? "no error message" :
exceptionToReport.getMessage();
if (isNativeQuery) {
requestLogger.logNativeQuery(
RequestLogLine.forNative(
@@ -359,7 +367,7 @@ public class AsyncQueryForwardingServlet extends
AsyncProxyServlet implements Qu
response.setContentType(MediaType.APPLICATION_JSON);
objectMapper.writeValue(
response.getOutputStream(),
- ImmutableMap.of("error", errorMessage)
+
serverConfig.getErrorResponseTransformStrategy().transformIfNeeded(exceptionToReport)
);
}
diff --git
a/services/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java
b/services/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java
index 9a92289..634f8a4 100644
---
a/services/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java
+++
b/services/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java
@@ -32,6 +32,8 @@ import com.google.inject.Module;
import com.google.inject.servlet.GuiceFilter;
import org.apache.calcite.avatica.Meta;
import org.apache.calcite.avatica.remote.Service;
+import
org.apache.druid.common.exception.AllowedRegexErrorResponseTransformStrategy;
+import org.apache.druid.common.exception.ErrorResponseTransformStrategy;
import org.apache.druid.common.utils.SocketUtil;
import org.apache.druid.guice.GuiceInjectors;
import org.apache.druid.guice.Jerseys;
@@ -50,6 +52,8 @@ import
org.apache.druid.query.DefaultGenericQueryMetricsFactory;
import org.apache.druid.query.Druids;
import org.apache.druid.query.MapQueryToolChestWarehouse;
import org.apache.druid.query.Query;
+import org.apache.druid.query.QueryException;
+import org.apache.druid.query.QueryInterruptedException;
import org.apache.druid.query.timeseries.TimeseriesQuery;
import org.apache.druid.segment.TestHelper;
import org.apache.druid.server.initialization.BaseJettyTest;
@@ -78,13 +82,18 @@ import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
+import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
+import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
@@ -224,6 +233,253 @@ public class AsyncQueryForwardingServletTest extends
BaseJettyTest
verifyServletCallsForQuery(query, false, hostFinder, new Properties());
}
+ @Test
+ public void testHandleExceptionWithFilterDisabled() throws Exception
+ {
+ String errorMessage = "test exception message";
+ ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
+ HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+ ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
+ Mockito.when(response.getOutputStream()).thenReturn(outputStream);
+ final AsyncQueryForwardingServlet servlet = new
AsyncQueryForwardingServlet(
+ new MapQueryToolChestWarehouse(ImmutableMap.of()),
+ mockMapper,
+ TestHelper.makeSmileMapper(),
+ null,
+ null,
+ null,
+ new NoopServiceEmitter(),
+ new NoopRequestLogger(),
+ new DefaultGenericQueryMetricsFactory(),
+ new AuthenticatorMapper(ImmutableMap.of()),
+ new Properties(),
+ new ServerConfig()
+ );
+ Exception testException = new IllegalStateException(errorMessage);
+ servlet.handleException(response, mockMapper, testException);
+ ArgumentCaptor<Exception> captor =
ArgumentCaptor.forClass(Exception.class);
+ Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream),
captor.capture());
+ Assert.assertTrue(captor.getValue() instanceof QueryException);
+ Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION,
((QueryException) captor.getValue()).getErrorCode());
+ Assert.assertEquals(errorMessage, captor.getValue().getMessage());
+ Assert.assertEquals(IllegalStateException.class.getName(),
((QueryException) captor.getValue()).getErrorClass());
+ }
+
+ @Test
+ public void testHandleExceptionWithFilterEnabled() throws Exception
+ {
+ String errorMessage = "test exception message";
+ ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
+ HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+ ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
+ Mockito.when(response.getOutputStream()).thenReturn(outputStream);
+ final AsyncQueryForwardingServlet servlet = new
AsyncQueryForwardingServlet(
+ new MapQueryToolChestWarehouse(ImmutableMap.of()),
+ mockMapper,
+ TestHelper.makeSmileMapper(),
+ null,
+ null,
+ null,
+ new NoopServiceEmitter(),
+ new NoopRequestLogger(),
+ new DefaultGenericQueryMetricsFactory(),
+ new AuthenticatorMapper(ImmutableMap.of()),
+ new Properties(),
+ new ServerConfig() {
+ @Override
+ public boolean isShowDetailedJettyErrors()
+ {
+ return true;
+ }
+
+ @Override
+ public ErrorResponseTransformStrategy
getErrorResponseTransformStrategy()
+ {
+ return new
AllowedRegexErrorResponseTransformStrategy(ImmutableList.of());
+ }
+ }
+ );
+ Exception testException = new IllegalStateException(errorMessage);
+ servlet.handleException(response, mockMapper, testException);
+ ArgumentCaptor<Exception> captor =
ArgumentCaptor.forClass(Exception.class);
+ Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream),
captor.capture());
+ Assert.assertTrue(captor.getValue() instanceof QueryException);
+ Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION,
((QueryException) captor.getValue()).getErrorCode());
+ Assert.assertNull(captor.getValue().getMessage());
+ Assert.assertNull(((QueryException) captor.getValue()).getErrorClass());
+ Assert.assertNull(((QueryException) captor.getValue()).getHost());
+ }
+
+ @Test
+ public void
testHandleExceptionWithFilterEnabledButMessageMatchAllowedRegex() throws
Exception
+ {
+ String errorMessage = "test exception message";
+ ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
+ HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+ ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
+ Mockito.when(response.getOutputStream()).thenReturn(outputStream);
+ final AsyncQueryForwardingServlet servlet = new
AsyncQueryForwardingServlet(
+ new MapQueryToolChestWarehouse(ImmutableMap.of()),
+ mockMapper,
+ TestHelper.makeSmileMapper(),
+ null,
+ null,
+ null,
+ new NoopServiceEmitter(),
+ new NoopRequestLogger(),
+ new DefaultGenericQueryMetricsFactory(),
+ new AuthenticatorMapper(ImmutableMap.of()),
+ new Properties(),
+ new ServerConfig() {
+ @Override
+ public boolean isShowDetailedJettyErrors()
+ {
+ return true;
+ }
+
+ @Override
+ public ErrorResponseTransformStrategy
getErrorResponseTransformStrategy()
+ {
+ return new
AllowedRegexErrorResponseTransformStrategy(ImmutableList.of("test .*"));
+ }
+ }
+ );
+ Exception testException = new IllegalStateException(errorMessage);
+ servlet.handleException(response, mockMapper, testException);
+ ArgumentCaptor<Exception> captor =
ArgumentCaptor.forClass(Exception.class);
+ Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream),
captor.capture());
+ Assert.assertTrue(captor.getValue() instanceof QueryException);
+ Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION,
((QueryException) captor.getValue()).getErrorCode());
+ Assert.assertEquals(errorMessage, captor.getValue().getMessage());
+ Assert.assertNull(((QueryException) captor.getValue()).getErrorClass());
+ Assert.assertNull(((QueryException) captor.getValue()).getHost());
+ }
+
+ @Test
+ public void testHandleQueryParseExceptionWithFilterDisabled() throws
Exception
+ {
+ String errorMessage = "test exception message";
+ ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+ HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+ ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
+ Mockito.when(response.getOutputStream()).thenReturn(outputStream);
+ final AsyncQueryForwardingServlet servlet = new
AsyncQueryForwardingServlet(
+ new MapQueryToolChestWarehouse(ImmutableMap.of()),
+ mockMapper,
+ TestHelper.makeSmileMapper(),
+ null,
+ null,
+ null,
+ new NoopServiceEmitter(),
+ new NoopRequestLogger(),
+ new DefaultGenericQueryMetricsFactory(),
+ new AuthenticatorMapper(ImmutableMap.of()),
+ new Properties(),
+ new ServerConfig()
+ );
+ IOException testException = new IOException(errorMessage);
+ servlet.handleQueryParseException(request, response, mockMapper,
testException, false);
+ ArgumentCaptor<Exception> captor =
ArgumentCaptor.forClass(Exception.class);
+ Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream),
captor.capture());
+ Assert.assertTrue(captor.getValue() instanceof QueryException);
+ Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION,
((QueryException) captor.getValue()).getErrorCode());
+ Assert.assertEquals(errorMessage, captor.getValue().getMessage());
+ Assert.assertEquals(IOException.class.getName(), ((QueryException)
captor.getValue()).getErrorClass());
+ }
+
+ @Test
+ public void testHandleQueryParseExceptionWithFilterEnabled() throws Exception
+ {
+ String errorMessage = "test exception message";
+ ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+ HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+ ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
+ Mockito.when(response.getOutputStream()).thenReturn(outputStream);
+ final AsyncQueryForwardingServlet servlet = new
AsyncQueryForwardingServlet(
+ new MapQueryToolChestWarehouse(ImmutableMap.of()),
+ mockMapper,
+ TestHelper.makeSmileMapper(),
+ null,
+ null,
+ null,
+ new NoopServiceEmitter(),
+ new NoopRequestLogger(),
+ new DefaultGenericQueryMetricsFactory(),
+ new AuthenticatorMapper(ImmutableMap.of()),
+ new Properties(),
+ new ServerConfig() {
+ @Override
+ public boolean isShowDetailedJettyErrors()
+ {
+ return true;
+ }
+
+ @Override
+ public ErrorResponseTransformStrategy
getErrorResponseTransformStrategy()
+ {
+ return new
AllowedRegexErrorResponseTransformStrategy(ImmutableList.of());
+ }
+ }
+ );
+ IOException testException = new IOException(errorMessage);
+ servlet.handleQueryParseException(request, response, mockMapper,
testException, false);
+ ArgumentCaptor<Exception> captor =
ArgumentCaptor.forClass(Exception.class);
+ Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream),
captor.capture());
+ Assert.assertTrue(captor.getValue() instanceof QueryException);
+ Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION,
((QueryException) captor.getValue()).getErrorCode());
+ Assert.assertNull(captor.getValue().getMessage());
+ Assert.assertNull(((QueryException) captor.getValue()).getErrorClass());
+ Assert.assertNull(((QueryException) captor.getValue()).getHost());
+ }
+
+ @Test
+ public void
testHandleQueryParseExceptionWithFilterEnabledButMessageMatchAllowedRegex()
throws Exception
+ {
+ String errorMessage = "test exception message";
+ ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+ HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+ ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
+ Mockito.when(response.getOutputStream()).thenReturn(outputStream);
+ final AsyncQueryForwardingServlet servlet = new
AsyncQueryForwardingServlet(
+ new MapQueryToolChestWarehouse(ImmutableMap.of()),
+ mockMapper,
+ TestHelper.makeSmileMapper(),
+ null,
+ null,
+ null,
+ new NoopServiceEmitter(),
+ new NoopRequestLogger(),
+ new DefaultGenericQueryMetricsFactory(),
+ new AuthenticatorMapper(ImmutableMap.of()),
+ new Properties(),
+ new ServerConfig() {
+ @Override
+ public boolean isShowDetailedJettyErrors()
+ {
+ return true;
+ }
+
+ @Override
+ public ErrorResponseTransformStrategy
getErrorResponseTransformStrategy()
+ {
+ return new
AllowedRegexErrorResponseTransformStrategy(ImmutableList.of("test .*"));
+ }
+ }
+ );
+ IOException testException = new IOException(errorMessage);
+ servlet.handleQueryParseException(request, response, mockMapper,
testException, false);
+ ArgumentCaptor<Exception> captor =
ArgumentCaptor.forClass(Exception.class);
+ Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream),
captor.capture());
+ Assert.assertTrue(captor.getValue() instanceof QueryException);
+ Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION,
((QueryException) captor.getValue()).getErrorCode());
+ Assert.assertEquals(errorMessage, captor.getValue().getMessage());
+ Assert.assertNull(((QueryException) captor.getValue()).getErrorClass());
+ Assert.assertNull(((QueryException) captor.getValue()).getHost());
+ }
+
/**
* Verifies that the Servlet calls the right methods the right number of
times.
*/
@@ -296,7 +552,8 @@ public class AsyncQueryForwardingServletTest extends
BaseJettyTest
new NoopRequestLogger(),
new DefaultGenericQueryMetricsFactory(),
new AuthenticatorMapper(ImmutableMap.of()),
- properties
+ properties,
+ new ServerConfig()
)
{
@Override
@@ -389,7 +646,8 @@ public class AsyncQueryForwardingServletTest extends
BaseJettyTest
new NoopRequestLogger(),
new DefaultGenericQueryMetricsFactory(),
new AuthenticatorMapper(ImmutableMap.of()),
- new Properties()
+ new Properties(),
+ new ServerConfig()
)
{
@Override
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 1d20f5d..2a070f4 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
@@ -26,6 +26,7 @@ import com.google.common.collect.Iterables;
import com.google.common.io.CountingOutputStream;
import com.google.inject.Inject;
import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.druid.common.exception.SanitizableException;
import org.apache.druid.guice.annotations.Json;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.StringUtils;
@@ -39,6 +40,7 @@ import org.apache.druid.query.QueryInterruptedException;
import org.apache.druid.query.QueryTimeoutException;
import org.apache.druid.query.QueryUnsupportedException;
import org.apache.druid.query.ResourceLimitExceededException;
+import org.apache.druid.server.initialization.ServerConfig;
import org.apache.druid.server.security.Access;
import org.apache.druid.server.security.AuthorizationUtils;
import org.apache.druid.server.security.AuthorizerMapper;
@@ -77,19 +79,22 @@ public class SqlResource
private final AuthorizerMapper authorizerMapper;
private final SqlLifecycleFactory sqlLifecycleFactory;
private final SqlLifecycleManager sqlLifecycleManager;
+ private final ServerConfig serverConfig;
@Inject
public SqlResource(
@Json ObjectMapper jsonMapper,
AuthorizerMapper authorizerMapper,
SqlLifecycleFactory sqlLifecycleFactory,
- SqlLifecycleManager sqlLifecycleManager
+ SqlLifecycleManager sqlLifecycleManager,
+ ServerConfig serverConfig
)
{
this.jsonMapper = Preconditions.checkNotNull(jsonMapper, "jsonMapper");
this.authorizerMapper = Preconditions.checkNotNull(authorizerMapper,
"authorizerMapper");
this.sqlLifecycleFactory = Preconditions.checkNotNull(sqlLifecycleFactory,
"sqlLifecycleFactory");
this.sqlLifecycleManager = Preconditions.checkNotNull(sqlLifecycleManager,
"sqlLifecycleManager");
+ this.serverConfig = serverConfig;
}
@POST
@@ -186,7 +191,7 @@ public class SqlResource
}
catch (ForbiddenException e) {
endLifecycleWithoutEmittingMetrics(sqlQueryId, lifecycle);
- throw e; // let ForbiddenExceptionMapper handle this
+ throw (ForbiddenException)
serverConfig.getErrorResponseTransformStrategy().transformIfNeeded(e); // let
ForbiddenExceptionMapper handle this
}
catch (Exception e) {
log.warn(e, "Failed to handle query: %s", sqlQuery);
@@ -230,11 +235,15 @@ public class SqlResource
sqlLifecycleManager.remove(sqlQueryId, lifecycle);
}
- private Response buildNonOkResponse(int status, Exception e) throws
JsonProcessingException
+ private Response buildNonOkResponse(int status, SanitizableException e)
throws JsonProcessingException
{
return Response.status(status)
.type(MediaType.APPLICATION_JSON_TYPE)
- .entity(jsonMapper.writeValueAsBytes(e))
+ .entity(
+ jsonMapper.writeValueAsBytes(
+
serverConfig.getErrorResponseTransformStrategy().transformIfNeeded(e)
+ )
+ )
.build();
}
diff --git a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java
b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java
index c7387ac..f52335d 100644
--- a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java
@@ -30,6 +30,8 @@ import com.google.common.util.concurrent.MoreExecutors;
import org.apache.calcite.avatica.SqlType;
import org.apache.calcite.tools.RelConversionException;
import org.apache.druid.common.config.NullHandling;
+import
org.apache.druid.common.exception.AllowedRegexErrorResponseTransformStrategy;
+import org.apache.druid.common.exception.ErrorResponseTransformStrategy;
import org.apache.druid.common.guava.SettableSupplier;
import org.apache.druid.jackson.DefaultObjectMapper;
import org.apache.druid.java.util.common.ISE;
@@ -121,6 +123,7 @@ public class SqlResourceTest extends CalciteTestBase
private HttpServletRequest req;
private ListeningExecutorService executorService;
private SqlLifecycleManager lifecycleManager;
+ private SqlLifecycleFactory sqlLifecycleFactory;
private CountDownLatch lifecycleAddLatch;
private final SettableSupplier<NonnullPair<CountDownLatch, Boolean>>
validateAndAuthorizeLatchSupplier = new SettableSupplier<>();
@@ -236,34 +239,36 @@ public class SqlResourceTest extends CalciteTestBase
}
};
final ServiceEmitter emitter = new NoopServiceEmitter();
- resource = new SqlResource(
- JSON_MAPPER,
- CalciteTests.TEST_AUTHORIZER_MAPPER,
- new SqlLifecycleFactory(
+ sqlLifecycleFactory = new SqlLifecycleFactory(
+ plannerFactory,
+ emitter,
+ testRequestLogger,
+ scheduler
+ )
+ {
+ @Override
+ public SqlLifecycle factorize()
+ {
+ return new TestSqlLifecycle(
plannerFactory,
emitter,
testRequestLogger,
- scheduler
- )
- {
- @Override
- public SqlLifecycle factorize()
- {
- return new TestSqlLifecycle(
- plannerFactory,
- emitter,
- testRequestLogger,
- scheduler,
- System.currentTimeMillis(),
- System.nanoTime(),
- validateAndAuthorizeLatchSupplier,
- planLatchSupplier,
- executeLatchSupplier,
- sequenceMapFnSupplier
- );
- }
- },
- lifecycleManager
+ scheduler,
+ System.currentTimeMillis(),
+ System.nanoTime(),
+ validateAndAuthorizeLatchSupplier,
+ planLatchSupplier,
+ executeLatchSupplier,
+ sequenceMapFnSupplier
+ );
+ }
+ };
+ resource = new SqlResource(
+ JSON_MAPPER,
+ CalciteTests.TEST_AUTHORIZER_MAPPER,
+ sqlLifecycleFactory,
+ lifecycleManager,
+ new ServerConfig()
);
}
@@ -969,6 +974,44 @@ public class SqlResourceTest extends CalciteTestBase
}
@Test
+ public void testUnsupportedQueryThrowsExceptionWithFilterResponse() throws
Exception
+ {
+ resource = new SqlResource(
+ JSON_MAPPER,
+ CalciteTests.TEST_AUTHORIZER_MAPPER,
+ sqlLifecycleFactory,
+ lifecycleManager,
+ new ServerConfig() {
+ @Override
+ public boolean isShowDetailedJettyErrors()
+ {
+ return true;
+ }
+ @Override
+ public ErrorResponseTransformStrategy
getErrorResponseTransformStrategy()
+ {
+ return new
AllowedRegexErrorResponseTransformStrategy(ImmutableList.of());
+ }
+ }
+ );
+
+ String errorMessage = "This will be support in Druid 9999";
+ SqlQuery badQuery = EasyMock.createMock(SqlQuery.class);
+ EasyMock.expect(badQuery.getQuery()).andReturn("SELECT ANSWER TO LIFE");
+
EasyMock.expect(badQuery.getContext()).andReturn(ImmutableMap.of("sqlQueryId",
"id"));
+ EasyMock.expect(badQuery.getParameterList()).andThrow(new
QueryUnsupportedException(errorMessage));
+ EasyMock.replay(badQuery);
+ final QueryException exception = doPost(badQuery).lhs;
+
+ Assert.assertNotNull(exception);
+ Assert.assertNull(exception.getMessage());
+ Assert.assertNull(exception.getHost());
+ Assert.assertEquals(exception.getErrorCode(),
QueryUnsupportedException.ERROR_CODE);
+ Assert.assertNull(exception.getErrorClass());
+ Assert.assertTrue(lifecycleManager.getAll("id").isEmpty());
+ }
+
+ @Test
public void testTooManyRequests() throws Exception
{
sleep = true;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]