From 9922cfdd1ec4e09eeaa9a3875fb8b7ffdd95a852 Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Sun, 31 Dec 2023 08:44:34 -0700 Subject: [PATCH 1/7] [JENKINS-72469] Avoid repeated tool downloads from misconfigured HTTP servers The Azul Systems content delivery network stopped providing the last-modified header in their URL responses. They only provide the ETag header. Add ETag support to the Jenkins FilePath URL download method so that if ETag is provided, we use the ETag value. If last-modified is provided and matches, we continue to honor it as well. https://issues.jenkins.io/browse/JENKINS-72469 has more details. https://community.jenkins.io/t/job-stuck-on-unpacking-global-jdk-tool/11272 also includes more details. Testing done * Automated test added to FilePathTest for code changes on the controller. The automated test confirms that even without a last-modified value, the later downloads are skipped if a matching ETag is received. The automated test also confirms that download is skipped if OK is received with a matching ETag. No automated test was added to confirm download on the agent because that path is not tested by any of the other test automation of this class. * Interactive test with the Azul Systems JDK installer on the controller. I created a tool installer for the Azul JDK. I verified that before this change it was downloaded each time the job was run. I verified that after the change it was downloaded only once. * Interactive test with the Azul Systems JDK installer on an agent. I created a tool installer for the Azul JDK. I verified that before this change it was downloaded each time the job was run. I verified that after the change it was downloaded only once. * Interactive test on the controller with a file download from an NGINX web server confirmed that the tool is downloaded once and then later runs of the job did not download the file again. --- core/src/main/java/hudson/FilePath.java | 23 +++++++++++++-- core/src/test/java/hudson/FilePathTest.java | 32 +++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index cdc057ef9c54..9a3913f3ca21 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -962,7 +962,7 @@ public Void invoke(File dir, VirtualChannel channel) throws IOException { * * * @param archive - * The resource that represents the tgz/zip file. This URL must support the {@code Last-Modified} header. + * The resource that represents the tgz/zip file. This URL must support the {@code Last-Modified} header or the {@code ETag} header. * (For example, you could use {@link ClassLoader#getResource}.) * @param listener * If non-null, a message will be printed to this listener once this method decides to @@ -984,12 +984,16 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen try { FilePath timestamp = this.child(".timestamp"); long lastModified = timestamp.lastModified(); + String etag = timestamp.exists() ? timestamp.readToString().replace("\"", "") : null; URLConnection con; try { con = ProxyConfiguration.open(archive); if (lastModified != 0) { con.setIfModifiedSince(lastModified); } + if (etag != null && !etag.isEmpty()) { + con.setRequestProperty("If-None-Match", etag); + } con.connect(); } catch (IOException x) { if (this.exists()) { @@ -1016,7 +1020,7 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen return false; } } - if (lastModified != 0) { + if (lastModified != 0 || (etag != null && !etag.isEmpty())) { if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { return false; } else if (responseCode != HttpURLConnection.HTTP_OK) { @@ -1027,8 +1031,15 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen } long sourceTimestamp = con.getLastModified(); + String resultEtag = con.getHeaderField("ETag"); + if (resultEtag != null && !resultEtag.isEmpty()) { + resultEtag = resultEtag.replace("\"", ""); + } if (this.exists()) { + if (resultEtag != null && !resultEtag.isEmpty() && etag != null && !etag.isEmpty() && resultEtag.contains(etag)) { + return false; // already up to date + } if (lastModified != 0 && sourceTimestamp == lastModified) return false; // already up to date this.deleteContents(); @@ -1042,6 +1053,10 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen // First try to download from the agent machine. try { act(new Unpack(archive)); + if (resultEtag != null && !resultEtag.isEmpty() && !resultEtag.equals(etag)) { + /* Store the ETag value in the timestamp file for later use */ + timestamp.write(resultEtag, "UTF-8"); + } timestamp.touch(sourceTimestamp); return true; } catch (IOException x) { @@ -1061,6 +1076,10 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen throw new IOException(String.format("Failed to unpack %s (%d bytes read of total %d)", archive, cis.getByteCount(), con.getContentLength()), e); } + if (resultEtag != null && !resultEtag.isEmpty() && !resultEtag.equals(etag)) { + /* Store the ETag value in the timestamp file for later use */ + timestamp.write(resultEtag, "UTF-8"); + } timestamp.touch(sourceTimestamp); return true; } catch (IOException e) { diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index fe152a25ba49..ed673d3d39a2 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -670,6 +670,38 @@ private static void assertValidateAntFileMask(String expected, FilePath d, Strin assertTrue(d.installIfNecessaryFrom(url, null, message)); } + @Issue("JENKINS-72469") + @Test public void installIfNecessaryWithoutLastModified() throws Exception { + final HttpURLConnection con = mock(HttpURLConnection.class); + // getLastModified == 0 when last-modified header is not returned + when(con.getLastModified()).thenReturn(0L); + // An Etag is provided by Azul CDN without last-modified header + when(con.getHeaderField("ETag")).thenReturn("An-opaque-ETag-string"); + when(con.getInputStream()).thenReturn(someZippedContent()); + + final URL url = someUrlToZipFile(con); + + File tmp = temp.getRoot(); + final FilePath d = new FilePath(tmp); + + /* Initial download expected to occur */ + assertTrue(d.installIfNecessaryFrom(url, null, "message if failed first download")); + + /* Timestamp last modified == 0 means the header was not provided */ + assertThat(d.child(".timestamp").lastModified(), is(0L)); + + /* Second download should not occur if JENKINS-72469 is fixed and NOT_MODIFIED is returned */ + when(con.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NOT_MODIFIED); + when(con.getInputStream()).thenReturn(someZippedContent()); + assertFalse(d.installIfNecessaryFrom(url, null, "message if failed second download")); + + /* Third download should not occur if JENKINS-72469 is fixed and OK is returned with matching ETag */ + /* Unexpected to receive an OK and a matching ETag from a real web server, but check for safety */ + when(con.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(con.getInputStream()).thenReturn(someZippedContent()); + assertFalse(d.installIfNecessaryFrom(url, null, "message if failed third download")); + } + private URL someUrlToZipFile(final URLConnection con) throws IOException { final URLStreamHandler urlHandler = new URLStreamHandler() { From cd0487e71fd55602fc07f42fd3783ccc1c51bf96 Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Sun, 31 Dec 2023 09:17:36 -0700 Subject: [PATCH 2/7] Use equals instead of contains to check ETag Don't risk that a substring of an earlier ETag might cause a later ETag to incorrectly assume it does not need to download a modified installer. --- core/src/main/java/hudson/FilePath.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 9a3913f3ca21..1f5995dafe1d 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -1037,7 +1037,7 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen } if (this.exists()) { - if (resultEtag != null && !resultEtag.isEmpty() && etag != null && !etag.isEmpty() && resultEtag.contains(etag)) { + if (resultEtag != null && !resultEtag.isEmpty() && etag != null && !etag.isEmpty() && resultEtag.equals(etag)) { return false; // already up to date } if (lastModified != 0 && sourceTimestamp == lastModified) From df05da5bb5bfde52278bb25f27f77f894a1fd2eb Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Sun, 31 Dec 2023 15:06:56 -0700 Subject: [PATCH 3/7] Use weak comparison for ETag values https://httpwg.org/specs/rfc9110.html#field.etag describes weak comparison cases and notes that content providers may provide weak or strong entity tags. Updated code to correctly compare weak and strong entity tags. Also improves the null checks based on the suggestions from @mawinter69 in https://github.com/jenkinsci/jenkins/pull/8814#discussion_r1438909824 --- core/src/main/java/hudson/FilePath.java | 39 +++++++++++++++------ core/src/test/java/hudson/FilePathTest.java | 20 +++++++++-- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 1f5995dafe1d..1da338d6e549 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -28,6 +28,7 @@ import static hudson.Util.fileToPath; import static hudson.Util.fixEmpty; +import static hudson.Util.fixEmptyAndTrim; import com.google.common.annotations.VisibleForTesting; import com.jcraft.jzlib.GZIPInputStream; @@ -984,14 +985,16 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen try { FilePath timestamp = this.child(".timestamp"); long lastModified = timestamp.lastModified(); - String etag = timestamp.exists() ? timestamp.readToString().replace("\"", "") : null; + // https://httpwg.org/specs/rfc9110.html#field.etag is the ETag specification + // Read previously stored ETag if timestamp is available + String etag = timestamp.exists() ? fixEmptyAndTrim(timestamp.readToString()) : null; URLConnection con; try { con = ProxyConfiguration.open(archive); if (lastModified != 0) { con.setIfModifiedSince(lastModified); } - if (etag != null && !etag.isEmpty()) { + if (etag != null) { con.setRequestProperty("If-None-Match", etag); } con.connect(); @@ -1020,7 +1023,7 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen return false; } } - if (lastModified != 0 || (etag != null && !etag.isEmpty())) { + if (lastModified != 0 || etag != null) { if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { return false; } else if (responseCode != HttpURLConnection.HTTP_OK) { @@ -1031,13 +1034,10 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen } long sourceTimestamp = con.getLastModified(); - String resultEtag = con.getHeaderField("ETag"); - if (resultEtag != null && !resultEtag.isEmpty()) { - resultEtag = resultEtag.replace("\"", ""); - } + String resultEtag = fixEmptyAndTrim(con.getHeaderField("ETag")); if (this.exists()) { - if (resultEtag != null && !resultEtag.isEmpty() && etag != null && !etag.isEmpty() && resultEtag.equals(etag)) { + if (equalETags(etag, resultEtag)) { return false; // already up to date } if (lastModified != 0 && sourceTimestamp == lastModified) @@ -1053,7 +1053,7 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen // First try to download from the agent machine. try { act(new Unpack(archive)); - if (resultEtag != null && !resultEtag.isEmpty() && !resultEtag.equals(etag)) { + if (resultEtag != null && !equalETags(etag, resultEtag)) { /* Store the ETag value in the timestamp file for later use */ timestamp.write(resultEtag, "UTF-8"); } @@ -1076,7 +1076,7 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen throw new IOException(String.format("Failed to unpack %s (%d bytes read of total %d)", archive, cis.getByteCount(), con.getContentLength()), e); } - if (resultEtag != null && !resultEtag.isEmpty() && !resultEtag.equals(etag)) { + if (resultEtag != null && !equalETags(etag, resultEtag)) { /* Store the ETag value in the timestamp file for later use */ timestamp.write(resultEtag, "UTF-8"); } @@ -1087,6 +1087,25 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen } } + /* Return true if etag1 equals etag2 as defined by the etag specification + https://httpwg.org/specs/rfc9110.html#field.etag + */ + private boolean equalETags(String etag1, String etag2) { + if (etag1 == null || etag2 == null) { + return false; + } + if (etag1.equals(etag2)) { + return true; + } + /* Weak tags are identified by leading characters "W/" as a marker */ + /* Weak tag marker must not be considered in tag comparison. + This implements the weak comparison in the specification at + https://httpwg.org/specs/rfc9110.html#field.etag */ + String opaqueTag1 = etag1.startsWith("W/") ? etag1.substring(2) : etag1; + String opaqueTag2 = etag2.startsWith("W/") ? etag2.substring(2) : etag2; + return opaqueTag1.equals(opaqueTag2); + } + // this reads from arbitrary URL private static final class Unpack extends MasterToSlaveFileCallable { private final URL archive; diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index ed673d3d39a2..8f2eea11620e 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -671,12 +671,28 @@ private static void assertValidateAntFileMask(String expected, FilePath d, Strin } @Issue("JENKINS-72469") - @Test public void installIfNecessaryWithoutLastModified() throws Exception { + @Test public void installIfNecessaryWithoutLastModifiedStrongValidator() throws Exception { + installIfNecessaryWithoutLastModified("\"An-ETag-strong-validator\""); + } + + @Issue("JENKINS-72469") + @Test public void installIfNecessaryWithoutLastModifiedStrongValidatorNoQuotes() throws Exception { + // This ETag is a violation of the spec at https://httpwg.org/specs/rfc9110.html#field.etag + // However, better safe to handle without quotes as well, just in case + installIfNecessaryWithoutLastModified("An-ETag-strong-validator-without-quotes"); + } + + @Issue("JENKINS-72469") + @Test public void installIfNecessaryWithoutLastModifiedWeakValidator() throws Exception { + installIfNecessaryWithoutLastModified("W/\"An-ETag-weak-validator\""); + } + + private void installIfNecessaryWithoutLastModified(String validator) throws Exception { final HttpURLConnection con = mock(HttpURLConnection.class); // getLastModified == 0 when last-modified header is not returned when(con.getLastModified()).thenReturn(0L); // An Etag is provided by Azul CDN without last-modified header - when(con.getHeaderField("ETag")).thenReturn("An-opaque-ETag-string"); + when(con.getHeaderField("ETag")).thenReturn(validator); when(con.getInputStream()).thenReturn(someZippedContent()); final URL url = someUrlToZipFile(con); From cc505701db87391cf795264f46b7fbc7fb67499d Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Sun, 31 Dec 2023 16:59:05 -0700 Subject: [PATCH 4/7] Test comparison of weak and strong validators --- core/src/test/java/hudson/FilePathTest.java | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index 8f2eea11620e..5a6e25e98799 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -672,22 +672,32 @@ private static void assertValidateAntFileMask(String expected, FilePath d, Strin @Issue("JENKINS-72469") @Test public void installIfNecessaryWithoutLastModifiedStrongValidator() throws Exception { - installIfNecessaryWithoutLastModified("\"An-ETag-strong-validator\""); + String validator = "\"An-ETag-strong-validator\""; + installIfNecessaryWithoutLastModified(validator, validator); } @Issue("JENKINS-72469") @Test public void installIfNecessaryWithoutLastModifiedStrongValidatorNoQuotes() throws Exception { // This ETag is a violation of the spec at https://httpwg.org/specs/rfc9110.html#field.etag // However, better safe to handle without quotes as well, just in case - installIfNecessaryWithoutLastModified("An-ETag-strong-validator-without-quotes"); + String validator = "An-ETag-strong-validator-without-quotes"; + installIfNecessaryWithoutLastModified(validator, validator); } @Issue("JENKINS-72469") @Test public void installIfNecessaryWithoutLastModifiedWeakValidator() throws Exception { - installIfNecessaryWithoutLastModified("W/\"An-ETag-weak-validator\""); + String validator = "W/\"An-ETag-weak-validator\""; + installIfNecessaryWithoutLastModified(validator, validator); } - private void installIfNecessaryWithoutLastModified(String validator) throws Exception { + @Issue("JENKINS-72469") + @Test public void installIfNecessaryWithoutLastModifiedWeakAndStrongValidators() throws Exception { + String validator = "\"An-ETag-validator\""; + String alternateValidator = "W/" + validator; + installIfNecessaryWithoutLastModified(validator, alternateValidator); + } + + private void installIfNecessaryWithoutLastModified(String validator, String alternateValidator) throws Exception { final HttpURLConnection con = mock(HttpURLConnection.class); // getLastModified == 0 when last-modified header is not returned when(con.getLastModified()).thenReturn(0L); @@ -709,12 +719,14 @@ private void installIfNecessaryWithoutLastModified(String validator) throws Exce /* Second download should not occur if JENKINS-72469 is fixed and NOT_MODIFIED is returned */ when(con.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NOT_MODIFIED); when(con.getInputStream()).thenReturn(someZippedContent()); + when(con.getHeaderField("ETag")).thenReturn(alternateValidator); assertFalse(d.installIfNecessaryFrom(url, null, "message if failed second download")); /* Third download should not occur if JENKINS-72469 is fixed and OK is returned with matching ETag */ /* Unexpected to receive an OK and a matching ETag from a real web server, but check for safety */ when(con.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); when(con.getInputStream()).thenReturn(someZippedContent()); + when(con.getHeaderField("ETag")).thenReturn(alternateValidator); assertFalse(d.installIfNecessaryFrom(url, null, "message if failed third download")); } From b068176848d02f0181653bbf4822009cb4fd065a Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Sun, 31 Dec 2023 17:19:05 -0700 Subject: [PATCH 5/7] Do not duplicate test args, more readable --- core/src/test/java/hudson/FilePathTest.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index 5a6e25e98799..b2fc842d537b 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -673,7 +673,7 @@ private static void assertValidateAntFileMask(String expected, FilePath d, Strin @Issue("JENKINS-72469") @Test public void installIfNecessaryWithoutLastModifiedStrongValidator() throws Exception { String validator = "\"An-ETag-strong-validator\""; - installIfNecessaryWithoutLastModified(validator, validator); + installIfNecessaryWithoutLastModified(validator); } @Issue("JENKINS-72469") @@ -681,13 +681,13 @@ private static void assertValidateAntFileMask(String expected, FilePath d, Strin // This ETag is a violation of the spec at https://httpwg.org/specs/rfc9110.html#field.etag // However, better safe to handle without quotes as well, just in case String validator = "An-ETag-strong-validator-without-quotes"; - installIfNecessaryWithoutLastModified(validator, validator); + installIfNecessaryWithoutLastModified(validator); } @Issue("JENKINS-72469") @Test public void installIfNecessaryWithoutLastModifiedWeakValidator() throws Exception { String validator = "W/\"An-ETag-weak-validator\""; - installIfNecessaryWithoutLastModified(validator, validator); + installIfNecessaryWithoutLastModified(validator); } @Issue("JENKINS-72469") @@ -697,6 +697,10 @@ private static void assertValidateAntFileMask(String expected, FilePath d, Strin installIfNecessaryWithoutLastModified(validator, alternateValidator); } + private void installIfNecessaryWithoutLastModified(String validator) throws Exception { + installIfNecessaryWithoutLastModified(validator, validator); + } + private void installIfNecessaryWithoutLastModified(String validator, String alternateValidator) throws Exception { final HttpURLConnection con = mock(HttpURLConnection.class); // getLastModified == 0 when last-modified header is not returned From 056ec5d04358ff05c61d045de89c5d7a4174ea98 Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Sun, 31 Dec 2023 19:29:06 -0700 Subject: [PATCH 6/7] Use better variable names in test Cover more branches in the equalEtags method as well --- core/src/test/java/hudson/FilePathTest.java | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index b2fc842d537b..7b2457d2120a 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -672,29 +672,36 @@ private static void assertValidateAntFileMask(String expected, FilePath d, Strin @Issue("JENKINS-72469") @Test public void installIfNecessaryWithoutLastModifiedStrongValidator() throws Exception { - String validator = "\"An-ETag-strong-validator\""; - installIfNecessaryWithoutLastModified(validator); + String strongValidator = "\"An-ETag-strong-validator\""; + installIfNecessaryWithoutLastModified(strongValidator); } @Issue("JENKINS-72469") @Test public void installIfNecessaryWithoutLastModifiedStrongValidatorNoQuotes() throws Exception { // This ETag is a violation of the spec at https://httpwg.org/specs/rfc9110.html#field.etag // However, better safe to handle without quotes as well, just in case - String validator = "An-ETag-strong-validator-without-quotes"; - installIfNecessaryWithoutLastModified(validator); + String strongValidator = "An-ETag-strong-validator-without-quotes"; + installIfNecessaryWithoutLastModified(strongValidator); } @Issue("JENKINS-72469") @Test public void installIfNecessaryWithoutLastModifiedWeakValidator() throws Exception { - String validator = "W/\"An-ETag-weak-validator\""; - installIfNecessaryWithoutLastModified(validator); + String weakValidator = "W/\"An-ETag-weak-validator\""; + installIfNecessaryWithoutLastModified(weakValidator); + } + + @Issue("JENKINS-72469") + @Test public void installIfNecessaryWithoutLastModifiedStrongAndWeakValidators() throws Exception { + String strongValidator = "\"An-ETag-validator\""; + String weakValidator = "W/" + strongValidator; + installIfNecessaryWithoutLastModified(strongValidator, weakValidator); } @Issue("JENKINS-72469") @Test public void installIfNecessaryWithoutLastModifiedWeakAndStrongValidators() throws Exception { - String validator = "\"An-ETag-validator\""; - String alternateValidator = "W/" + validator; - installIfNecessaryWithoutLastModified(validator, alternateValidator); + String weakValidator = "W/" + strongValidator; + String strongValidator = "\"An-ETag-validator\""; + installIfNecessaryWithoutLastModified(weakValidator, strongValidator); } private void installIfNecessaryWithoutLastModified(String validator) throws Exception { From aa3578d5fd7f126cb23316c5ad83cd8e7d5dc7f4 Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Sun, 31 Dec 2023 20:27:19 -0700 Subject: [PATCH 7/7] Fix variable declaration order --- core/src/test/java/hudson/FilePathTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index 7b2457d2120a..54d37c2b3dcd 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -699,8 +699,8 @@ private static void assertValidateAntFileMask(String expected, FilePath d, Strin @Issue("JENKINS-72469") @Test public void installIfNecessaryWithoutLastModifiedWeakAndStrongValidators() throws Exception { - String weakValidator = "W/" + strongValidator; String strongValidator = "\"An-ETag-validator\""; + String weakValidator = "W/" + strongValidator; installIfNecessaryWithoutLastModified(weakValidator, strongValidator); }