Browse Source

Correct warning header to be compliant

The warning header used by Elasticsearch for delivering deprecation
warnings has a specific format (RFC 7234, section 5.5). The format
specifies that the warning header should be of the form

    warn-code warn-agent warn-text [warn-date]

Here, the warn-code is a three-digit code which communicates various
meanings. The warn-agent is a string used to identify the source of the
warning (either a host:port combination, or some other identifier). The
warn-text is quoted string which conveys the semantic meaning of the
warning. The warn-date is an optional quoted date that can be in a few
different formats.

This commit corrects the warning header within Elasticsearch to follow
this specification. We use the warn-code 299 which means a
"miscellaneous persistent warning." For the warn-agent, we use the
version of Elasticsearch that produced the warning. The warn-text is
unchanged from what we deliver today, but is wrapped in quotes as
specified (this is important as a problem that exists today is that
multiple warnings can not be split by comma to obtain the individual
warnings as the warnings might themselves contain commas). For the
warn-date, we use the RFC 1123 format.

Relates #23275
Jason Tedor 8 năm trước cách đây
mục cha
commit
577e6a5e14

+ 88 - 13
core/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java

@@ -21,13 +21,21 @@ package org.elasticsearch.common.logging;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+import org.elasticsearch.Build;
+import org.elasticsearch.Version;
 import org.elasticsearch.common.SuppressLoggerChecks;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.Iterator;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * A logger that logs deprecation notices.
@@ -36,14 +44,6 @@ public class DeprecationLogger {
 
     private final Logger logger;
 
-    /**
-     * The "Warning" Header comes from RFC-7234. As the RFC describes, it's generally used for caching purposes, but it can be
-     * used for <em>any</em> warning.
-     *
-     * https://tools.ietf.org/html/rfc7234#section-5.5
-     */
-    public static final String WARNING_HEADER = "Warning";
-
     /**
      * This is set once by the {@code Node} constructor, but it uses {@link CopyOnWriteArraySet} to ensure that tests can run in parallel.
      * <p>
@@ -112,6 +112,57 @@ public class DeprecationLogger {
         deprecated(THREAD_CONTEXT, msg, params);
     }
 
+    /*
+     * RFC7234 specifies the warning format as warn-code <space> warn-agent <space> "warn-text" [<space> "warn-date"]. Here, warn-code is a
+     * three-digit number with various standard warn codes specified. The warn code 299 is apt for our purposes as it represents a
+     * miscellaneous persistent warning (can be presented to a human, or logged, and must not be removed by a cache). The warn-agent is an
+     * arbitrary token; here we use the Elasticsearch version and build hash. The warn text must be quoted. The warn-date is an optional
+     * quoted field that can be in a variety of specified date formats; here we use RFC 1123 format.
+     */
+    private static final String WARNING_FORMAT =
+            String.format(
+                    Locale.ROOT,
+                    "299 Elasticsearch-%s%s-%s ",
+                    Version.CURRENT.toString(),
+                    Build.CURRENT.isSnapshot() ? "-SNAPSHOT" : "",
+                    Build.CURRENT.shortHash()) +
+                    "\"%s\" \"%s\"";
+
+    private static final ZoneId GMT = ZoneId.of("GMT");
+
+    /**
+     * Regular expression to test if a string matches the RFC7234 specification for warning headers. This pattern assumes that the warn code
+     * is always 299. Further, this pattern assumes that the warn agent represents a version of Elasticsearch including the build hash.
+     */
+    public static Pattern WARNING_HEADER_PATTERN = Pattern.compile(
+            "299 " + // warn code
+                    "Elasticsearch-\\d+\\.\\d+\\.\\d+(?:-(?:alpha|beta|rc)\\d+)?(?:-SNAPSHOT)?-(?:[a-f0-9]{7}|Unknown) " + // warn agent
+                    "\"((?:\t| |!|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x80-\\xff]|\\\\|\\\\\")*)\" " + // quoted warning value, captured
+                    // quoted RFC 1123 date format
+                    "\"" + // opening quote
+                    "(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), " + // weekday
+                    "\\d{2} " + // 2-digit day
+                    "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) " + // month
+                    "\\d{4} " + // 4-digit year
+                    "\\d{2}:\\d{2}:\\d{2} " + // (two-digit hour):(two-digit minute):(two-digit second)
+                    "GMT" + // GMT
+                    "\""); // closing quote
+
+    /**
+     * Extracts the warning value from the value of a warning header that is formatted according to RFC 7234. That is, given a string
+     * {@code 299 Elasticsearch-6.0.0 "warning value" "Sat, 25 Feb 2017 10:27:43 GMT"}, the return value of this method would be {@code
+     * warning value}.
+     *
+     * @param s the value of a warning header formatted according to RFC 7234.
+     * @return the extracted warning value
+     */
+    public static String extractWarningValueFromWarningHeader(final String s) {
+        final Matcher matcher = WARNING_HEADER_PATTERN.matcher(s);
+        final boolean matches = matcher.matches();
+        assert matches;
+        return matcher.group(1);
+    }
+
     /**
      * Logs a deprecated message to the deprecation log, as well as to the local {@link ThreadContext}.
      *
@@ -120,16 +171,19 @@ public class DeprecationLogger {
      * @param params The parameters used to fill in the message, if any exist.
      */
     @SuppressLoggerChecks(reason = "safely delegates to logger")
-    void deprecated(Set<ThreadContext> threadContexts, String message, Object... params) {
-        Iterator<ThreadContext> iterator = threadContexts.iterator();
+    void deprecated(final Set<ThreadContext> threadContexts, final String message, final Object... params) {
+        final Iterator<ThreadContext> iterator = threadContexts.iterator();
 
         if (iterator.hasNext()) {
             final String formattedMessage = LoggerMessageFormat.format(message, params);
-
+            final String warningHeaderValue = formatWarning(formattedMessage);
+            assert WARNING_HEADER_PATTERN.matcher(warningHeaderValue).matches();
+            assert extractWarningValueFromWarningHeader(warningHeaderValue).equals(escape(formattedMessage));
             while (iterator.hasNext()) {
                 try {
-                    iterator.next().addResponseHeader(WARNING_HEADER, formattedMessage);
-                } catch (IllegalStateException e) {
+                    final ThreadContext next = iterator.next();
+                    next.addResponseHeader("Warning", warningHeaderValue, DeprecationLogger::extractWarningValueFromWarningHeader);
+                } catch (final IllegalStateException e) {
                     // ignored; it should be removed shortly
                 }
             }
@@ -139,4 +193,25 @@ public class DeprecationLogger {
         }
     }
 
+    /**
+     * Format a warning string in the proper warning format by prepending a warn code, warn agent, wrapping the warning string in quotes,
+     * and appending the RFC 1123 date.
+     *
+     * @param s the warning string to format
+     * @return a warning value formatted according to RFC 7234
+     */
+    public static String formatWarning(final String s) {
+        return String.format(Locale.ROOT, WARNING_FORMAT, escape(s), DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(GMT)));
+    }
+
+    /**
+     * Escape backslashes and quotes in the specified string.
+     *
+     * @param s the string to escape
+     * @return the escaped string
+     */
+    public static String escape(String s) {
+        return s.replaceAll("(\\\\|\")", "\\\\$1");
+    }
+
 }

+ 24 - 7
core/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java

@@ -34,7 +34,9 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -257,12 +259,25 @@ public final class ThreadContext implements Closeable, Writeable {
     }
 
     /**
-     * Add the <em>unique</em> response {@code value} for the specified {@code key}.
-     * <p>
-     * Any duplicate {@code value} is ignored.
+     * Add the {@code value} for the specified {@code key} Any duplicate {@code value} is ignored.
+     *
+     * @param key         the header name
+     * @param value       the header value
+     */
+    public void addResponseHeader(final String key, final String value) {
+        addResponseHeader(key, value, v -> v);
+    }
+
+    /**
+     * Add the {@code value} for the specified {@code key} with the specified {@code uniqueValue} used for de-duplication. Any duplicate
+     * {@code value} after applying {@code uniqueValue} is ignored.
+     *
+     * @param key         the header name
+     * @param value       the header value
+     * @param uniqueValue the function that produces de-duplication values
      */
-    public void addResponseHeader(String key, String value) {
-        threadLocal.set(threadLocal.get().putResponse(key, value));
+    public void addResponseHeader(final String key, final String value, final Function<String, String> uniqueValue) {
+        threadLocal.set(threadLocal.get().putResponse(key, value, uniqueValue));
     }
 
     /**
@@ -396,14 +411,16 @@ public final class ThreadContext implements Closeable, Writeable {
             return new ThreadContextStruct(requestHeaders, newResponseHeaders, transientHeaders);
         }
 
-        private ThreadContextStruct putResponse(String key, String value) {
+        private ThreadContextStruct putResponse(final String key, final String value, final Function<String, String> uniqueValue) {
             assert value != null;
 
             final Map<String, List<String>> newResponseHeaders = new HashMap<>(this.responseHeaders);
             final List<String> existingValues = newResponseHeaders.get(key);
 
             if (existingValues != null) {
-                if (existingValues.contains(value)) {
+                final Set<String> existingUniqueValues = existingValues.stream().map(uniqueValue).collect(Collectors.toSet());
+                assert existingValues.size() == existingUniqueValues.size();
+                if (existingUniqueValues.contains(uniqueValue.apply(value))) {
                     return this;
                 }
 

+ 68 - 36
core/src/test/java/org/elasticsearch/common/logging/DeprecationLoggerTests.java

@@ -18,9 +18,11 @@
  */
 package org.elasticsearch.common.logging;
 
+import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.hamcrest.RegexMatcher;
 
 import java.io.IOException;
 import java.util.Collections;
@@ -28,17 +30,22 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.IntStream;
 
-import static org.hamcrest.Matchers.not;
+import static org.elasticsearch.common.logging.DeprecationLogger.WARNING_HEADER_PATTERN;
+import static org.elasticsearch.test.hamcrest.RegexMatcher.matches;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.not;
 
 /**
  * Tests {@link DeprecationLogger}
  */
 public class DeprecationLoggerTests extends ESTestCase {
 
+    private static final RegexMatcher warningValueMatcher = matches(WARNING_HEADER_PATTERN.pattern());
+
     private final DeprecationLogger logger = new DeprecationLogger(Loggers.getLogger(getClass()));
 
     @Override
@@ -48,43 +55,42 @@ public class DeprecationLoggerTests extends ESTestCase {
     }
 
     public void testAddsHeaderWithThreadContext() throws IOException {
-        String msg = "A simple message [{}]";
-        String param = randomAsciiOfLengthBetween(1, 5);
-        String formatted = LoggerMessageFormat.format(msg, (Object)param);
-
         try (ThreadContext threadContext = new ThreadContext(Settings.EMPTY)) {
-            Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
+            final Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
 
-            logger.deprecated(threadContexts, msg, param);
+            final String param = randomAsciiOfLengthBetween(1, 5);
+            logger.deprecated(threadContexts, "A simple message [{}]", param);
 
-            Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
+            final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
 
-            assertEquals(1, responseHeaders.size());
-            assertEquals(formatted, responseHeaders.get(DeprecationLogger.WARNING_HEADER).get(0));
+            assertThat(responseHeaders.size(), equalTo(1));
+            final List<String> responses = responseHeaders.get("Warning");
+            assertThat(responses, hasSize(1));
+            assertThat(responses.get(0), warningValueMatcher);
+            assertThat(responses.get(0), containsString("\"A simple message [" + param + "]\""));
         }
     }
 
     public void testAddsCombinedHeaderWithThreadContext() throws IOException {
-        String msg = "A simple message [{}]";
-        String param = randomAsciiOfLengthBetween(1, 5);
-        String formatted = LoggerMessageFormat.format(msg, (Object)param);
-        String formatted2 = randomAsciiOfLengthBetween(1, 10);
-
         try (ThreadContext threadContext = new ThreadContext(Settings.EMPTY)) {
-            Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
+            final Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
 
-            logger.deprecated(threadContexts, msg, param);
-            logger.deprecated(threadContexts, formatted2);
+            final String param = randomAsciiOfLengthBetween(1, 5);
+            logger.deprecated(threadContexts, "A simple message [{}]", param);
+            final String second = randomAsciiOfLengthBetween(1, 10);
+            logger.deprecated(threadContexts, second);
 
-            Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
+            final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
 
             assertEquals(1, responseHeaders.size());
 
-            List<String> responses = responseHeaders.get(DeprecationLogger.WARNING_HEADER);
+            final List<String> responses = responseHeaders.get("Warning");
 
             assertEquals(2, responses.size());
-            assertEquals(formatted, responses.get(0));
-            assertEquals(formatted2, responses.get(1));
+            assertThat(responses.get(0), warningValueMatcher);
+            assertThat(responses.get(0), containsString("\"A simple message [" + param + "]\""));
+            assertThat(responses.get(1), warningValueMatcher);
+            assertThat(responses.get(1), containsString("\"" + second + "\""));
         }
     }
 
@@ -93,28 +99,30 @@ public class DeprecationLoggerTests extends ESTestCase {
         final String unexpected = "testCannotRemoveThreadContext";
 
         try (ThreadContext threadContext = new ThreadContext(Settings.EMPTY)) {
-            // NOTE: by adding it to the logger, we allow any concurrent test to write to it (from their own threads)
             DeprecationLogger.setThreadContext(threadContext);
-
             logger.deprecated(expected);
 
-            Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
-            List<String> responses = responseHeaders.get(DeprecationLogger.WARNING_HEADER);
+            {
+                final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
+                final List<String> responses = responseHeaders.get("Warning");
 
-            // ensure it works (note: concurrent tests may be adding to it, but in different threads, so it should have no impact)
-            assertThat(responses, hasSize(atLeast(1)));
-            assertThat(responses, hasItem(equalTo(expected)));
+                assertThat(responses, hasSize(1));
+                assertThat(responses.get(0), warningValueMatcher);
+                assertThat(responses.get(0), containsString(expected));
+            }
 
             DeprecationLogger.removeThreadContext(threadContext);
-
             logger.deprecated(unexpected);
 
-            responseHeaders = threadContext.getResponseHeaders();
-            responses = responseHeaders.get(DeprecationLogger.WARNING_HEADER);
+            {
+                final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
+                final List<String> responses = responseHeaders.get("Warning");
 
-            assertThat(responses, hasSize(atLeast(1)));
-            assertThat(responses, hasItem(expected));
-            assertThat(responses, not(hasItem(unexpected)));
+                assertThat(responses, hasSize(1));
+                assertThat(responses.get(0), warningValueMatcher);
+                assertThat(responses.get(0), containsString(expected));
+                assertThat(responses.get(0), not(containsString(unexpected)));
+            }
         }
     }
 
@@ -158,4 +166,28 @@ public class DeprecationLoggerTests extends ESTestCase {
         }
     }
 
+    public void testWarningValueFromWarningHeader() throws InterruptedException {
+        final String s = randomAsciiOfLength(16);
+        final String first = DeprecationLogger.formatWarning(s);
+        assertThat(DeprecationLogger.extractWarningValueFromWarningHeader(first), equalTo(s));
+    }
+
+    public void testEscape() {
+        assertThat(DeprecationLogger.escape("\\"), equalTo("\\\\"));
+        assertThat(DeprecationLogger.escape("\""), equalTo("\\\""));
+        assertThat(DeprecationLogger.escape("\\\""), equalTo("\\\\\\\""));
+        assertThat(DeprecationLogger.escape("\"foo\\bar\""),equalTo("\\\"foo\\\\bar\\\""));
+        // test that characters other than '\' and '"' are left unchanged
+        String chars = "\t !" + range(0x23, 0x5b + 1) + range(0x5d, 0x73 + 1) + range(0x80, 0xff + 1);
+        final String s = new CodepointSetGenerator(chars.toCharArray()).ofCodePointsLength(random(), 16, 16);
+        assertThat(DeprecationLogger.escape(s), equalTo(s));
+    }
+
+    private String range(int lowerInclusive, int upperInclusive) {
+        return IntStream
+                .range(lowerInclusive, upperInclusive + 1)
+                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+                .toString();
+    }
+
 }

+ 12 - 0
core/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java

@@ -19,6 +19,7 @@
 package org.elasticsearch.common.util.concurrent;
 
 import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.logging.DeprecationLogger;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.test.ESTestCase;
 
@@ -178,6 +179,14 @@ public class ThreadContextTests extends ESTestCase {
             threadContext.addResponseHeader("foo", "bar");
         }
 
+        final String value = DeprecationLogger.formatWarning("qux");
+        threadContext.addResponseHeader("baz", value, DeprecationLogger::extractWarningValueFromWarningHeader);
+        // pretend that another thread created the same response at a different time
+        if (randomBoolean()) {
+            final String duplicateValue = DeprecationLogger.formatWarning("qux");
+            threadContext.addResponseHeader("baz", duplicateValue, DeprecationLogger::extractWarningValueFromWarningHeader);
+        }
+
         threadContext.addResponseHeader("Warning", "One is the loneliest number");
         threadContext.addResponseHeader("Warning", "Two can be as bad as one");
         if (expectThird) {
@@ -186,11 +195,14 @@ public class ThreadContextTests extends ESTestCase {
 
         final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
         final List<String> foo = responseHeaders.get("foo");
+        final List<String> baz = responseHeaders.get("baz");
         final List<String> warnings = responseHeaders.get("Warning");
         final int expectedWarnings = expectThird ? 3 : 2;
 
         assertThat(foo, hasSize(1));
+        assertThat(baz, hasSize(1));
         assertEquals("bar", foo.get(0));
+        assertEquals(value, baz.get(0));
         assertThat(warnings, hasSize(expectedWarnings));
         assertThat(warnings, hasItem(equalTo("One is the loneliest number")));
         assertThat(warnings, hasItem(equalTo("Two can be as bad as one")));

+ 3 - 3
plugins/discovery-azure-classic/src/main/java/org/elasticsearch/plugin/discovery/azure/classic/AzureDiscoveryPlugin.java

@@ -102,9 +102,9 @@ public class AzureDiscoveryPlugin extends Plugin implements DiscoveryPlugin {
         // setting existed. This check looks for the legacy setting, and sets hosts provider if set
         String discoveryType = DiscoveryModule.DISCOVERY_TYPE_SETTING.get(settings);
         if (discoveryType.equals(AZURE)) {
-            deprecationLogger.deprecated("Using " + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() +
-                " setting to set hosts provider is deprecated. " +
-                "Set \"" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + AZURE + "\" instead");
+            deprecationLogger.deprecated("using [" + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() +
+                "] to set hosts provider is deprecated; " +
+                "set \"" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + AZURE + "\" instead");
             if (DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.exists(settings) == false) {
                 return Settings.builder().put(DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey(), AZURE).build();
             }

+ 3 - 3
plugins/discovery-ec2/src/main/java/org/elasticsearch/plugin/discovery/ec2/Ec2DiscoveryPlugin.java

@@ -165,9 +165,9 @@ public class Ec2DiscoveryPlugin extends Plugin implements DiscoveryPlugin, Close
         // setting existed. This check looks for the legacy setting, and sets hosts provider if set
         String discoveryType = DiscoveryModule.DISCOVERY_TYPE_SETTING.get(settings);
         if (discoveryType.equals(EC2)) {
-            deprecationLogger.deprecated("Using " + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() +
-                " setting to set hosts provider is deprecated. " +
-                "Set \"" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + EC2 + "\" instead");
+            deprecationLogger.deprecated("using [" + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() +
+                "] setting to set hosts provider is deprecated; " +
+                "set [" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + EC2 + "] instead");
             if (DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.exists(settings) == false) {
                 builder.put(DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey(), EC2).build();
             }

+ 48 - 50
plugins/repository-s3/src/test/java/org/elasticsearch/cloud/aws/AwsS3ServiceImplTests.java

@@ -23,7 +23,6 @@ import com.amazonaws.ClientConfiguration;
 import com.amazonaws.Protocol;
 import com.amazonaws.auth.AWSCredentials;
 import com.amazonaws.auth.AWSCredentialsProvider;
-
 import org.elasticsearch.common.settings.MockSecureSettings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.repositories.s3.S3Repository;
@@ -65,8 +64,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
             .put(AwsS3Service.SECRET_SETTING.getKey(), "aws_secret")
             .build();
         launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "aws_key", "aws_secret");
-        assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(AwsS3Service.KEY_SETTING, AwsS3Service.SECRET_SETTING);
     }
 
     public void testAWSCredentialsWithElasticsearchS3SettingsBackcompat() {
@@ -75,8 +73,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
             .put(AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey(), "s3_secret")
             .build();
         launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "s3_key", "s3_secret");
-        assertWarnings("[" + AwsS3Service.CLOUD_S3.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(AwsS3Service.CLOUD_S3.KEY_SETTING, AwsS3Service.CLOUD_S3.SECRET_SETTING);
     }
 
     public void testAWSCredentialsWithElasticsearchAwsAndS3SettingsBackcompat() {
@@ -87,10 +84,11 @@ public class AwsS3ServiceImplTests extends ESTestCase {
             .put(AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey(), "s3_secret")
             .build();
         launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "s3_key", "s3_secret");
-        assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(
+                AwsS3Service.KEY_SETTING,
+                AwsS3Service.SECRET_SETTING,
+                AwsS3Service.CLOUD_S3.KEY_SETTING,
+                AwsS3Service.CLOUD_S3.SECRET_SETTING);
     }
 
     public void testAWSCredentialsWithElasticsearchRepositoriesSettingsBackcompat() {
@@ -99,8 +97,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
             .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
             .build();
         launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "repositories_key", "repositories_secret");
-        assertWarnings("[" + S3Repository.Repositories.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + S3Repository.Repositories.SECRET_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(S3Repository.Repositories.KEY_SETTING, S3Repository.Repositories.SECRET_SETTING);
     }
 
     public void testAWSCredentialsWithElasticsearchAwsAndRepositoriesSettingsBackcompat() {
@@ -111,10 +108,11 @@ public class AwsS3ServiceImplTests extends ESTestCase {
             .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
             .build();
         launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "repositories_key", "repositories_secret");
-        assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated",
-                       "[" + S3Repository.Repositories.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + S3Repository.Repositories.SECRET_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(
+                AwsS3Service.KEY_SETTING,
+                AwsS3Service.SECRET_SETTING,
+                S3Repository.Repositories.KEY_SETTING,
+                S3Repository.Repositories.SECRET_SETTING);
     }
 
     public void testAWSCredentialsWithElasticsearchAwsAndS3AndRepositoriesSettingsBackcompat() {
@@ -127,12 +125,13 @@ public class AwsS3ServiceImplTests extends ESTestCase {
             .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
             .build();
         launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "repositories_key", "repositories_secret");
-        assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey() + "] setting was deprecated",
-                       "[" + S3Repository.Repositories.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + S3Repository.Repositories.SECRET_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(
+                AwsS3Service.KEY_SETTING,
+                AwsS3Service.SECRET_SETTING,
+                AwsS3Service.CLOUD_S3.KEY_SETTING,
+                AwsS3Service.CLOUD_S3.SECRET_SETTING,
+                S3Repository.Repositories.KEY_SETTING,
+                S3Repository.Repositories.SECRET_SETTING);
     }
 
     public void testAWSCredentialsWithElasticsearchRepositoriesSettingsAndRepositorySettingsBackcompat() {
@@ -142,8 +141,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
             .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
             .build();
         launchAWSCredentialsWithElasticsearchSettingsTest(repositorySettings, settings, "repository_key", "repository_secret");
-        assertWarnings("[" + S3Repository.Repository.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + S3Repository.Repository.SECRET_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(S3Repository.Repository.KEY_SETTING, S3Repository.Repository.SECRET_SETTING);
     }
 
     public void testAWSCredentialsWithElasticsearchAwsAndRepositoriesSettingsAndRepositorySettingsBackcompat() {
@@ -155,8 +153,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
             .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
             .build();
         launchAWSCredentialsWithElasticsearchSettingsTest(repositorySettings, settings, "repository_key", "repository_secret");
-        assertWarnings("[" + S3Repository.Repository.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + S3Repository.Repository.SECRET_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(S3Repository.Repository.KEY_SETTING, S3Repository.Repository.SECRET_SETTING);
     }
 
     public void testAWSCredentialsWithElasticsearchAwsAndS3AndRepositoriesSettingsAndRepositorySettingsBackcompat() {
@@ -170,8 +167,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
             .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
             .build();
         launchAWSCredentialsWithElasticsearchSettingsTest(repositorySettings, settings, "repository_key", "repository_secret");
-        assertWarnings("[" + S3Repository.Repository.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + S3Repository.Repository.SECRET_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(S3Repository.Repository.KEY_SETTING, S3Repository.Repository.SECRET_SETTING);
     }
 
     protected void launchAWSCredentialsWithElasticsearchSettingsTest(Settings singleRepositorySettings, Settings settings,
@@ -215,13 +211,14 @@ public class AwsS3ServiceImplTests extends ESTestCase {
             .build();
         launchAWSConfigurationTest(settings, Settings.EMPTY, Protocol.HTTP, "aws_proxy_host", 8080, "aws_proxy_username",
             "aws_proxy_password", "AWS3SignerType", 3, false, 10000);
-        assertWarnings("[" + AwsS3Service.PROXY_USERNAME_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.PROXY_PASSWORD_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.PROTOCOL_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.PROXY_HOST_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.PROXY_PORT_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.SIGNER_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.READ_TIMEOUT.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(
+                AwsS3Service.PROXY_USERNAME_SETTING,
+                AwsS3Service.PROXY_PASSWORD_SETTING,
+                AwsS3Service.PROTOCOL_SETTING,
+                AwsS3Service.PROXY_HOST_SETTING,
+                AwsS3Service.PROXY_PORT_SETTING,
+                AwsS3Service.SIGNER_SETTING,
+                AwsS3Service.READ_TIMEOUT);
     }
 
     public void testAWSConfigurationWithAwsAndS3SettingsBackcompat() {
@@ -243,20 +240,21 @@ public class AwsS3ServiceImplTests extends ESTestCase {
             .build();
         launchAWSConfigurationTest(settings, Settings.EMPTY, Protocol.HTTPS, "s3_proxy_host", 8081, "s3_proxy_username",
             "s3_proxy_password", "NoOpSignerType", 3, false, 10000);
-        assertWarnings("[" + AwsS3Service.PROXY_USERNAME_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.PROXY_PASSWORD_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.PROTOCOL_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.PROXY_HOST_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.PROXY_PORT_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.SIGNER_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.READ_TIMEOUT.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.PROXY_USERNAME_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.PROXY_PASSWORD_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.PROTOCOL_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.PROXY_HOST_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.PROXY_PORT_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.SIGNER_SETTING.getKey() + "] setting was deprecated",
-                       "[" + AwsS3Service.CLOUD_S3.READ_TIMEOUT.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(
+                AwsS3Service.PROXY_USERNAME_SETTING,
+                AwsS3Service.PROXY_PASSWORD_SETTING,
+                AwsS3Service.PROTOCOL_SETTING,
+                AwsS3Service.PROXY_HOST_SETTING,
+                AwsS3Service.PROXY_PORT_SETTING,
+                AwsS3Service.SIGNER_SETTING,
+                AwsS3Service.READ_TIMEOUT,
+                AwsS3Service.CLOUD_S3.PROXY_USERNAME_SETTING,
+                AwsS3Service.CLOUD_S3.PROXY_PASSWORD_SETTING,
+                AwsS3Service.CLOUD_S3.PROTOCOL_SETTING,
+                AwsS3Service.CLOUD_S3.PROXY_HOST_SETTING,
+                AwsS3Service.CLOUD_S3.PROXY_PORT_SETTING,
+                AwsS3Service.CLOUD_S3.SIGNER_SETTING,
+                AwsS3Service.CLOUD_S3.READ_TIMEOUT);
     }
 
     public void testGlobalMaxRetries() {
@@ -338,13 +336,13 @@ public class AwsS3ServiceImplTests extends ESTestCase {
     public void testEndpointSettingBackcompat() {
         assertEndpoint(generateRepositorySettings("repository_key", "repository_secret", "repository.endpoint", null),
             Settings.EMPTY, "repository.endpoint");
-        assertWarnings("[" + S3Repository.Repository.ENDPOINT_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(S3Repository.Repository.ENDPOINT_SETTING);
         Settings settings = Settings.builder()
             .put(S3Repository.Repositories.ENDPOINT_SETTING.getKey(), "repositories.endpoint")
             .build();
         assertEndpoint(generateRepositorySettings("repository_key", "repository_secret", null, null), settings,
             "repositories.endpoint");
-        assertWarnings("[" + S3Repository.Repositories.ENDPOINT_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(S3Repository.Repositories.ENDPOINT_SETTING);
     }
 
     private void assertEndpoint(Settings repositorySettings, Settings settings,

+ 3 - 4
plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java

@@ -19,8 +19,6 @@
 
 package org.elasticsearch.repositories.s3;
 
-import java.io.IOException;
-
 import com.amazonaws.services.s3.AbstractAmazonS3;
 import com.amazonaws.services.s3.AmazonS3;
 import org.elasticsearch.cloud.aws.AwsS3Service;
@@ -35,6 +33,8 @@ import org.elasticsearch.repositories.RepositoryException;
 import org.elasticsearch.test.ESTestCase;
 import org.hamcrest.Matchers;
 
+import java.io.IOException;
+
 import static org.elasticsearch.repositories.s3.S3Repository.Repositories;
 import static org.elasticsearch.repositories.s3.S3Repository.Repository;
 import static org.elasticsearch.repositories.s3.S3Repository.getValue;
@@ -78,8 +78,7 @@ public class S3RepositoryTests extends ESTestCase {
                      getValue(Settings.EMPTY, globalSettings, Repository.KEY_SETTING, Repositories.KEY_SETTING));
         assertEquals(new SecureString("".toCharArray()),
                      getValue(Settings.EMPTY, Settings.EMPTY, Repository.KEY_SETTING, Repositories.KEY_SETTING));
-        assertWarnings("[" + Repository.KEY_SETTING.getKey() + "] setting was deprecated",
-                       "[" + Repositories.KEY_SETTING.getKey() + "] setting was deprecated");
+        assertSettingDeprecations(Repository.KEY_SETTING, Repositories.KEY_SETTING);
     }
 
     public void testInvalidChunkBufferSizeSettings() throws IOException {

+ 16 - 5
test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java

@@ -59,6 +59,7 @@ import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.logging.DeprecationLogger;
 import org.elasticsearch.common.logging.Loggers;
+import org.elasticsearch.common.settings.Setting;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.transport.TransportAddress;
 import org.elasticsearch.common.util.MockBigArrays;
@@ -134,10 +135,8 @@ import java.util.stream.Collectors;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
 import static org.elasticsearch.common.util.CollectionUtils.arrayAsArrayList;
-import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.hasItem;
 
 /**
  * Base testcase for randomized unit testing with Elasticsearch
@@ -297,21 +296,33 @@ public abstract class ESTestCase extends LuceneTestCase {
         //Check that there are no unaccounted warning headers. These should be checked with {@link #assertWarnings(String...)} in the
         //appropriate test
         try {
-            final List<String> warnings = threadContext.getResponseHeaders().get(DeprecationLogger.WARNING_HEADER);
+            final List<String> warnings = threadContext.getResponseHeaders().get("Warning");
             assertNull("unexpected warning headers", warnings);
         } finally {
             resetDeprecationLogger();
         }
     }
 
+    protected final void assertSettingDeprecations(Setting... settings) {
+        assertWarnings(
+                Arrays
+                        .stream(settings)
+                        .map(Setting::getKey)
+                        .map(k -> "[" + k + "] setting was deprecated in Elasticsearch and will be removed in a future release! " +
+                                "See the breaking changes documentation for the next major version.")
+                        .toArray(String[]::new));
+    }
+
     protected final void assertWarnings(String... expectedWarnings) {
         if (enableWarningsCheck() == false) {
             throw new IllegalStateException("unable to check warning headers if the test is not set to do so");
         }
         try {
-            final List<String> actualWarnings = threadContext.getResponseHeaders().get(DeprecationLogger.WARNING_HEADER);
+            final List<String> actualWarnings = threadContext.getResponseHeaders().get("Warning");
+            final Set<String> actualWarningValues =
+                    actualWarnings.stream().map(DeprecationLogger::extractWarningValueFromWarningHeader).collect(Collectors.toSet());
             for (String msg : expectedWarnings) {
-                assertThat(actualWarnings, hasItem(containsString(msg)));
+                assertTrue(actualWarningValues.contains(DeprecationLogger.escape(msg)));
             }
             assertEquals("Expected " + expectedWarnings.length + " warnings but found " + actualWarnings.size() + "\nExpected: "
                     + Arrays.asList(expectedWarnings) + "\nActual: " + actualWarnings,

+ 42 - 22
test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java

@@ -16,12 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
 package org.elasticsearch.test.rest.yaml.section;
 
 import org.apache.logging.log4j.Logger;
 import org.elasticsearch.common.ParsingException;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.logging.DeprecationLogger;
 import org.elasticsearch.common.logging.Loggers;
 import org.elasticsearch.common.xcontent.NamedXContentRegistry;
 import org.elasticsearch.common.xcontent.XContentLocation;
@@ -39,10 +41,13 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.stream.Collectors;
 
 import static java.util.Collections.emptyList;
 import static java.util.Collections.unmodifiableList;
 import static org.elasticsearch.common.collect.Tuple.tuple;
+import static org.elasticsearch.common.logging.DeprecationLogger.WARNING_HEADER_PATTERN;
 import static org.elasticsearch.test.hamcrest.RegexMatcher.matches;
 import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.equalTo;
@@ -247,33 +252,48 @@ public class DoSection implements ExecutableSection {
     /**
      * Check that the response contains only the warning headers that we expect.
      */
-    void checkWarningHeaders(List<String> warningHeaders) {
-        StringBuilder failureMessage = null;
+    void checkWarningHeaders(final List<String> warningHeaders) {
+        final List<String> unexpected = new ArrayList<>();
+        final List<String> unmatched = new ArrayList<>();
+        final List<String> missing = new ArrayList<>();
         // LinkedHashSet so that missing expected warnings come back in a predictable order which is nice for testing
-        Set<String> expected = new LinkedHashSet<>(expectedWarningHeaders);
-        for (String header : warningHeaders) {
-            if (expected.remove(header)) {
-                // Was expected, all good.
-                continue;
-            }
-            if (failureMessage == null) {
-                failureMessage = new StringBuilder("got unexpected warning headers [");
-            }
-            failureMessage.append('\n').append(header);
-        }
-        if (false == expected.isEmpty()) {
-            if (failureMessage == null) {
-                failureMessage = new StringBuilder();
+        final Set<String> expected =
+                new LinkedHashSet<>(expectedWarningHeaders.stream().map(DeprecationLogger::escape).collect(Collectors.toList()));
+        for (final String header : warningHeaders) {
+            final Matcher matcher = WARNING_HEADER_PATTERN.matcher(header);
+            final boolean matches = matcher.matches();
+            if (matches) {
+                final String message = matcher.group(1);
+                if (expected.remove(message) == false) {
+                    unexpected.add(header);
+                }
             } else {
-                failureMessage.append("\n] ");
+                unmatched.add(header);
             }
-            failureMessage.append("didn't get expected warning headers [");
-            for (String header : expected) {
-                failureMessage.append('\n').append(header);
+        }
+        if (expected.isEmpty() == false) {
+            for (final String header : expected) {
+                missing.add(header);
             }
         }
-        if (failureMessage != null) {
-            fail(failureMessage + "\n]");
+
+        if (unexpected.isEmpty() == false || unmatched.isEmpty() == false || missing.isEmpty() == false) {
+            final StringBuilder failureMessage = new StringBuilder();
+            appendBadHeaders(failureMessage, unexpected, "got unexpected warning header" + (unexpected.size() > 1 ? "s" : ""));
+            appendBadHeaders(failureMessage, unmatched, "got unmatched warning header" + (unmatched.size() > 1 ? "s" : ""));
+            appendBadHeaders(failureMessage, missing, "did not get expected warning header" + (missing.size() > 1 ? "s" : ""));
+            fail(failureMessage.toString());
+        }
+
+    }
+
+    private void appendBadHeaders(final StringBuilder sb, final List<String> headers, final String message) {
+        if (headers.isEmpty() == false) {
+            sb.append(message).append(" [\n");
+            for (final String header : headers) {
+                sb.append("\t").append(header).append("\n");
+            }
+            sb.append("]\n");
         }
     }
 

+ 68 - 23
test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/DoSectionTests.java

@@ -19,6 +19,9 @@
 
 package org.elasticsearch.test.rest.yaml.section;
 
+import org.elasticsearch.Version;
+import org.elasticsearch.common.hash.MessageDigests;
+import org.elasticsearch.common.logging.DeprecationLogger;
 import org.elasticsearch.common.xcontent.XContent;
 import org.elasticsearch.common.xcontent.XContentLocation;
 import org.elasticsearch.common.xcontent.XContentParser;
@@ -26,6 +29,7 @@ import org.elasticsearch.common.xcontent.yaml.YamlXContent;
 import org.hamcrest.MatcherAssert;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
 import java.util.Map;
 
@@ -37,39 +41,80 @@ import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
 
 public class DoSectionTests extends AbstractClientYamlTestFragmentParserTestCase {
+
     public void testWarningHeaders() throws IOException {
-        DoSection section = new DoSection(new XContentLocation(1, 1));
+        {
+            final DoSection section = new DoSection(new XContentLocation(1, 1));
 
-        // No warning headers doesn't throw an exception
-        section.checkWarningHeaders(emptyList());
+            // No warning headers doesn't throw an exception
+            section.checkWarningHeaders(emptyList());
+        }
 
+        final String testHeader = DeprecationLogger.formatWarning("test");
+        final String anotherHeader = DeprecationLogger.formatWarning("another");
+        final String someMoreHeader = DeprecationLogger.formatWarning("some more");
+        final String catHeader = DeprecationLogger.formatWarning("cat");
         // Any warning headers fail
-        AssertionError e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(singletonList("test")));
-        assertEquals("got unexpected warning headers [\ntest\n]", e.getMessage());
-        e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList("test", "another", "some more")));
-        assertEquals("got unexpected warning headers [\ntest\nanother\nsome more\n]", e.getMessage());
+        {
+            final DoSection section = new DoSection(new XContentLocation(1, 1));
+
+            final AssertionError one = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(singletonList(testHeader)));
+            assertEquals("got unexpected warning header [\n\t" + testHeader + "\n]\n", one.getMessage());
+
+            final AssertionError multiple =
+                    expectThrows(
+                            AssertionError.class,
+                            () -> section.checkWarningHeaders(Arrays.asList(testHeader, anotherHeader, someMoreHeader)));
+            assertEquals(
+                    "got unexpected warning headers [\n\t" +
+                            testHeader + "\n\t" +
+                            anotherHeader + "\n\t" +
+                            someMoreHeader + "\n]\n",
+                    multiple.getMessage());
+        }
 
         // But not when we expect them
-        section.setExpectedWarningHeaders(singletonList("test"));
-        section.checkWarningHeaders(singletonList("test"));
-        section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
-        section.checkWarningHeaders(Arrays.asList("test", "another", "some more"));
+        {
+            final DoSection section = new DoSection(new XContentLocation(1, 1));
+            section.setExpectedWarningHeaders(singletonList("test"));
+            section.checkWarningHeaders(singletonList(testHeader));
+        }
+        {
+            final DoSection section = new DoSection(new XContentLocation(1, 1));
+            section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
+            section.checkWarningHeaders(Arrays.asList(testHeader, anotherHeader, someMoreHeader));
+        }
 
         // But if you don't get some that you did expect, that is an error
-        section.setExpectedWarningHeaders(singletonList("test"));
-        e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList()));
-        assertEquals("didn't get expected warning headers [\ntest\n]", e.getMessage());
-        section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
-        e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList()));
-        assertEquals("didn't get expected warning headers [\ntest\nanother\nsome more\n]", e.getMessage());
-        e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList("test", "some more")));
-        assertEquals("didn't get expected warning headers [\nanother\n]", e.getMessage());
+        {
+            final DoSection section = new DoSection(new XContentLocation(1, 1));
+            section.setExpectedWarningHeaders(singletonList("test"));
+            final AssertionError e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList()));
+            assertEquals("did not get expected warning header [\n\ttest\n]\n", e.getMessage());
+        }
+        {
+            final DoSection section = new DoSection(new XContentLocation(1, 1));
+            section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
+
+            final AssertionError multiple = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList()));
+            assertEquals("did not get expected warning headers [\n\ttest\n\tanother\n\tsome more\n]\n", multiple.getMessage());
+
+            final AssertionError one =
+                    expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList(testHeader, someMoreHeader)));
+            assertEquals("did not get expected warning header [\n\tanother\n]\n", one.getMessage());
+        }
 
         // It is also an error if you get some warning you want and some you don't want
-        section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
-        e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList("test", "cat")));
-        assertEquals("got unexpected warning headers [\ncat\n] didn't get expected warning headers [\nanother\nsome more\n]",
-                e.getMessage());
+        {
+            final DoSection section = new DoSection(new XContentLocation(1, 1));
+            section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
+            final AssertionError e =
+                    expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList(testHeader, catHeader)));
+            assertEquals("got unexpected warning header [\n\t" +
+                            catHeader + "\n]\n" +
+                            "did not get expected warning headers [\n\tanother\n\tsome more\n]\n",
+                    e.getMessage());
+        }
     }
 
     public void testParseDoSectionNoBody() throws Exception {