diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java index b9deeca138..20f9599945 100644 --- a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java +++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java @@ -39,6 +39,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; @@ -84,6 +85,10 @@ public class WebServer { private static final Pattern CACHE_JS_FILE = Pattern.compile("/(" + STRONG_NAME + ").cache.js$"); + private static final Pattern ACCEPT_ENCODING_SPEC = Pattern.compile( + "^\\s*([!#$%&'*+.^_`|~0-9A-Za-z-]+|\\*)\\s*(?:;\\s*q\\s*=\\s*" + + "(0(?:\\.\\d{0,3})?|1(?:\\.0{0,3})?))?\\s*$"); + private static final MimeTypes MIME_TYPES = new MimeTypes(); private static final String TIME_IN_THE_PAST = "Mon, 01 Jan 1990 00:00:00 GMT"; @@ -372,9 +377,11 @@ public void send(HttpServletRequest request, HttpServletResponse response, TreeL } if (contentEncoding != null) { - if (!request.getHeader("Accept-Encoding").contains("gzip")) { - response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); - logger.log(TreeLogger.WARN, "client doesn't accept gzip; bailing"); + if (!acceptsGzipEncoding(request.getHeader("Accept-Encoding"))) { + response.setHeader("Accept-Encoding", "gzip"); + response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + logger.log(TreeLogger.WARN, + "client doesn't accept gzip and no uncompressed representation exists; bailing"); return; } response.setHeader("Content-Encoding", "gzip"); @@ -543,6 +550,43 @@ static String guessMimeType(String filename) { return mimeType != null ? mimeType : ""; } + /* visible for testing */ + static boolean acceptsGzipEncoding(String acceptEncodingHeader) { + if (acceptEncodingHeader == null) { + // RFC 9110: if Accept-Encoding is absent, any content coding is acceptable. + return true; + } + if (acceptEncodingHeader.trim().isEmpty()) { + return false; + } + + Double gzipQValue = null; + Double wildcardQValue = null; + + for (String encodingSpec : acceptEncodingHeader.split(",")) { + Matcher matcher = ACCEPT_ENCODING_SPEC.matcher(encodingSpec); + if (!matcher.matches()) { + continue; + } + + String encoding = matcher.group(1).toLowerCase(Locale.ROOT); + String qValueText = matcher.group(2); + double qValue = qValueText == null ? 1.0 : Double.parseDouble(qValueText); + + if (encoding.equals("gzip")) { + gzipQValue = qValue; + } else if (encoding.equals("*")) { + wildcardQValue = qValue; + } + } + + if (gzipQValue != null) { + return gzipQValue > 0.0; + } + + return wildcardQValue != null && wildcardQValue > 0.0; + } + /** * Returns the binding properties from the web page where dev mode is being used. (As passed in * by dev_mode_on.js in a JSONP request to "/recompile".) diff --git a/dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerAcceptEncodingTest.java b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerAcceptEncodingTest.java new file mode 100644 index 0000000000..e35e0e516c --- /dev/null +++ b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerAcceptEncodingTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2026 The GWT Project Authors. + * + * Licensed 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 com.google.gwt.dev.codeserver; + +import junit.framework.TestCase; + +/** + * Tests request Accept-Encoding parsing for serving gzip-compressed responses. + */ +public class WebServerAcceptEncodingTest extends TestCase { + + public void testAcceptsGzipEncodingWhenHeaderIsAbsent() { + assertTrue(WebServer.acceptsGzipEncoding(null)); + } + + public void testAcceptsGzipEncodingRejectsEmptyHeaderValue() { + assertFalse(WebServer.acceptsGzipEncoding("")); + assertFalse(WebServer.acceptsGzipEncoding(" ")); + } + + public void testAcceptsGzipEncodingAcceptsSimpleGzip() { + assertTrue(WebServer.acceptsGzipEncoding("gzip")); + assertTrue(WebServer.acceptsGzipEncoding("deflate, gzip")); + assertTrue(WebServer.acceptsGzipEncoding("GZIP")); + } + + public void testAcceptsGzipEncodingRejectsSubstringMatches() { + assertFalse(WebServer.acceptsGzipEncoding("xgzip")); + assertFalse(WebServer.acceptsGzipEncoding("gzip-alt")); + } + + public void testAcceptsGzipEncodingRejectsExplicitGzipZeroQValue() { + assertFalse(WebServer.acceptsGzipEncoding("gzip;q=0")); + assertFalse(WebServer.acceptsGzipEncoding("deflate, gzip; q=0.0")); + } + + public void testAcceptsGzipEncodingHonorsWildcardWhenGzipAbsent() { + assertTrue(WebServer.acceptsGzipEncoding("*")); + assertTrue(WebServer.acceptsGzipEncoding("br;q=0.2, *;q=0.7")); + assertFalse(WebServer.acceptsGzipEncoding("*;q=0")); + } + + public void testAcceptsGzipEncodingPrefersExplicitGzipOverWildcard() { + assertFalse(WebServer.acceptsGzipEncoding("gzip;q=0, *;q=1")); + assertTrue(WebServer.acceptsGzipEncoding("gzip;q=1, *;q=0")); + } + + public void testAcceptsGzipEncodingRejectsInvalidQualityValues() { + assertFalse(WebServer.acceptsGzipEncoding("gzip;q=not-a-number")); + assertFalse(WebServer.acceptsGzipEncoding("gzip;q=1.1")); + assertFalse(WebServer.acceptsGzipEncoding("gzip;q=0.1234")); + } + + public void testAcceptsGzipEncodingRejectsUnsupportedParameters() { + assertFalse(WebServer.acceptsGzipEncoding("gzip;level=9")); + } +}