diff --git a/.github/workflows/full-check.yml b/.github/workflows/full-check.yml
index 2e7a2644bc..27bf3b861f 100644
--- a/.github/workflows/full-check.yml
+++ b/.github/workflows/full-check.yml
@@ -31,7 +31,8 @@ jobs:
       - name: Checkout GWT tools into a sibling directory
         uses: actions/checkout@v4
         with:
-          repository: 'gwtproject/tools'
+          repository: 'zbynek/tools'
+          ref: 'htmlunit-4'
           path: 'tools'
       - name: Set up JDK ${{ matrix.java-version }}
         # GWT requires Java 11+ to build
diff --git a/.github/workflows/quick-check.yml b/.github/workflows/quick-check.yml
index 09977bd4d6..fcdd80d037 100644
--- a/.github/workflows/quick-check.yml
+++ b/.github/workflows/quick-check.yml
@@ -19,7 +19,8 @@ jobs:
       - name: Checkout GWT tools into a sibling directory
         uses: actions/checkout@v4
         with:
-          repository: 'gwtproject/tools'
+          repository: 'zbynek/tools'
+          ref: 'htmlunit-4'
           path: 'tools'
       - name: Set up JDK ${{ matrix.java-version }}
         # GWT presently requires Java 11+ to build
diff --git a/dev/build.xml b/dev/build.xml
index 2884648095..bf5c0354c8 100755
--- a/dev/build.xml
+++ b/dev/build.xml
@@ -85,19 +85,20 @@
           
           
           
-          
-          
-          
-          
-          
-          
-          
-          
-          
-          
-          
-          
-          
+          
+          
+          
+          
+          
+          
+          
+          
+          
+          
+          
+          
+          
+          
           
           
           
@@ -155,20 +156,20 @@
           
           
-          
-          
-          
-          
-          
-          
-          
-          
-          
-          
-          
-          
-          
+          
+          
+          
+          
+          
+          
+          
+          
+          
+          
+          
+          
+          
+          
           
           
           
diff --git a/dev/core/src/com/google/gwt/dev/shell/HostedModePluginObject.java b/dev/core/src/com/google/gwt/dev/shell/HostedModePluginObject.java
index 98d5051d8f..c9aeaef585 100644
--- a/dev/core/src/com/google/gwt/dev/shell/HostedModePluginObject.java
+++ b/dev/core/src/com/google/gwt/dev/shell/HostedModePluginObject.java
@@ -17,17 +17,15 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 
-import com.gargoylesoftware.htmlunit.WebClient;
-import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
-import com.gargoylesoftware.htmlunit.javascript.host.Window;
-
-import net.sourceforge.htmlunit.corejs.javascript.Context;
-import net.sourceforge.htmlunit.corejs.javascript.Function;
-import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
-import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
+import org.htmlunit.WebClient;
+import org.htmlunit.corejs.javascript.Context;
+import org.htmlunit.corejs.javascript.Function;
+import org.htmlunit.corejs.javascript.Scriptable;
+import org.htmlunit.corejs.javascript.ScriptableObject;
+import org.htmlunit.javascript.JavaScriptEngine;
+import org.htmlunit.javascript.host.Window;
 
 import java.io.IOException;
-import java.util.Collections;
 
 /**
  * HTMLUnit object that represents the hosted-mode plugin.
@@ -187,15 +185,6 @@ public boolean connect(String url, String sessionKey, String address,
       return false;
     }
     // TODO: add whitelist and default-port support?
-
-    // We know that legacy dev mode is running, we need to tell HtmlUnit that it is safe
-    // to permit plain Java objects to leak into JS - the JavaObject type will return a
-    // Object[] with a success boolean and a value, and HtmlUnit will guard against this.
-    // The simplest way to do that here is to mark java.lang.Object as the java equivalent
-    // of some JS type - the name of the type doesn't matter.
-    webClient.setActiveXObjectMap(Collections.singletonMap(
-            "GwtLegacyDevModeExceptionOrReturnValue", "java.lang.Object"));
-
     try {
       HtmlUnitSessionHandler htmlUnitSessionHandler = new HtmlUnitSessionHandler(
           window, jsEngine, webClient);
diff --git a/dev/core/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java b/dev/core/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java
index 75abb3aea3..8b93757bcc 100644
--- a/dev/core/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java
+++ b/dev/core/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java
@@ -23,23 +23,21 @@
 import com.google.gwt.dev.shell.BrowserChannelClient.SessionHandlerClient;
 import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 
-import com.gargoylesoftware.htmlunit.ScriptException;
-import com.gargoylesoftware.htmlunit.ScriptResult;
-import com.gargoylesoftware.htmlunit.WebClient;
-import com.gargoylesoftware.htmlunit.WebWindow;
-import com.gargoylesoftware.htmlunit.html.HtmlPage;
-import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
-import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
-import com.gargoylesoftware.htmlunit.javascript.SimpleScriptableProxy;
-import com.gargoylesoftware.htmlunit.javascript.host.Window;
-
-import net.sourceforge.htmlunit.corejs.javascript.ConsString;
-import net.sourceforge.htmlunit.corejs.javascript.Context;
-import net.sourceforge.htmlunit.corejs.javascript.Function;
-import net.sourceforge.htmlunit.corejs.javascript.JavaScriptException;
-import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
-import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
-import net.sourceforge.htmlunit.corejs.javascript.Undefined;
+import org.htmlunit.ScriptException;
+import org.htmlunit.ScriptResult;
+import org.htmlunit.WebClient;
+import org.htmlunit.WebWindow;
+import org.htmlunit.corejs.javascript.ConsString;
+import org.htmlunit.corejs.javascript.Context;
+import org.htmlunit.corejs.javascript.Function;
+import org.htmlunit.corejs.javascript.JavaScriptException;
+import org.htmlunit.corejs.javascript.Scriptable;
+import org.htmlunit.corejs.javascript.ScriptableObject;
+import org.htmlunit.corejs.javascript.Undefined;
+import org.htmlunit.html.HtmlPage;
+import org.htmlunit.javascript.HtmlUnitScriptableProxy;
+import org.htmlunit.javascript.JavaScriptEngine;
+import org.htmlunit.javascript.host.Window;
 
 import java.util.Arrays;
 import java.util.HashMap;
@@ -185,13 +183,14 @@ public ExceptionOrReturnValue invoke(BrowserChannelClient channel, Value thisObj
       jsThis = window;
     } else {
       Object obj = makeJsvalFromValue(jsContext, thisObj);
+      if (obj instanceof HtmlUnitScriptableProxy) {
+        obj = ((HtmlUnitScriptableProxy>) obj).getDelegee();
+      }
       if (obj instanceof ScriptableObject) {
         jsThis = (ScriptableObject) obj;
-      } else if (obj instanceof SimpleScriptableProxy>) {
-        jsThis = ((SimpleScriptableProxy) obj).getDelegee();
       } else {
         logger.log(TreeLogger.ERROR, "Unable to convert " + obj + " to either "
-            + " ScriptableObject or SimpleScriptableProxy");
+            + " ScriptableObject or HtmlUnitScriptableProxy");
         return new ExceptionOrReturnValue(true, new Value(null));
       }
     }
@@ -289,14 +288,14 @@ public Value makeValueFromJsval(Context jsContext, Object value) {
       return returnVal;
     }
     if (value instanceof Scriptable) {
-      if (value instanceof SimpleScriptableProxy) {
+      if (value instanceof HtmlUnitScriptableProxy) {
         // HtmlUnit will return proxies to java for the window/document objects,
         // so that those objects can work after navigating away from the page.
         // However, GWTTestCase operates inside a single page session, so we
         // can unwrap these proxies to get the real instance. Without doing
         // this, the refToJsObject mapping would indicate that an object might
         // not equal itself
-        value = ((SimpleScriptableProxy>) value).getDelegee();
+        value = ((HtmlUnitScriptableProxy>) value).getDelegee();
       }
       if (value instanceof ScriptableObject) {
         /*
diff --git a/dev/core/src/com/google/gwt/dev/shell/JavaObject.java b/dev/core/src/com/google/gwt/dev/shell/JavaObject.java
index 19a31f9604..86a91fcfa1 100644
--- a/dev/core/src/com/google/gwt/dev/shell/JavaObject.java
+++ b/dev/core/src/com/google/gwt/dev/shell/JavaObject.java
@@ -21,11 +21,11 @@
 import com.google.gwt.dev.shell.BrowserChannel.SessionHandler.ExceptionOrReturnValue;
 import com.google.gwt.dev.shell.BrowserChannel.Value;
 
-import net.sourceforge.htmlunit.corejs.javascript.Context;
-import net.sourceforge.htmlunit.corejs.javascript.Function;
-import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
-import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
-import net.sourceforge.htmlunit.corejs.javascript.Undefined;
+import org.htmlunit.corejs.javascript.Context;
+import org.htmlunit.corejs.javascript.Function;
+import org.htmlunit.corejs.javascript.Scriptable;
+import org.htmlunit.corejs.javascript.ScriptableObject;
+import org.htmlunit.corejs.javascript.Undefined;
 
 import java.io.IOException;
 
diff --git a/dev/core/test/com/google/gwt/core/ext/linker/impl/SelectionScriptJavaScriptTest.java b/dev/core/test/com/google/gwt/core/ext/linker/impl/SelectionScriptJavaScriptTest.java
index b1b1e513ca..0bbc97169e 100644
--- a/dev/core/test/com/google/gwt/core/ext/linker/impl/SelectionScriptJavaScriptTest.java
+++ b/dev/core/test/com/google/gwt/core/ext/linker/impl/SelectionScriptJavaScriptTest.java
@@ -18,14 +18,14 @@
 
 import com.google.gwt.core.ext.linker.LinkerUtils;
 
-import com.gargoylesoftware.htmlunit.AlertHandler;
-import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
-import com.gargoylesoftware.htmlunit.MockWebConnection;
-import com.gargoylesoftware.htmlunit.Page;
-import com.gargoylesoftware.htmlunit.WebClient;
-
 import junit.framework.TestCase;
 
+import org.htmlunit.AlertHandler;
+import org.htmlunit.FailingHttpStatusCodeException;
+import org.htmlunit.MockWebConnection;
+import org.htmlunit.Page;
+import org.htmlunit.WebClient;
+
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
diff --git a/maven/poms/gwt/gwt-dev/pom-template.xml b/maven/poms/gwt/gwt-dev/pom-template.xml
index c8f3419350..70d0c0655f 100644
--- a/maven/poms/gwt/gwt-dev/pom-template.xml
+++ b/maven/poms/gwt/gwt-dev/pom-template.xml
@@ -95,7 +95,7 @@
             
         
         
-            net.sourceforge.htmlunit
+            org.htmlunit
             htmlunit
         
         
diff --git a/maven/poms/gwt/pom-template.xml b/maven/poms/gwt/pom-template.xml
index b165744e3e..0e6d70be8d 100644
--- a/maven/poms/gwt/pom-template.xml
+++ b/maven/poms/gwt/pom-template.xml
@@ -141,9 +141,9 @@
                 63.1
             
             
-                net.sourceforge.htmlunit
+                org.htmlunit
                 htmlunit
-                2.55.0
+                4.11.1
             
             
                 org.w3c.css
diff --git a/user/src/com/google/gwt/junit/RunStyleHtmlUnit.java b/user/src/com/google/gwt/junit/RunStyleHtmlUnit.java
index 73815427f5..1a09f21233 100644
--- a/user/src/com/google/gwt/junit/RunStyleHtmlUnit.java
+++ b/user/src/com/google/gwt/junit/RunStyleHtmlUnit.java
@@ -20,30 +20,26 @@
 import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
 import com.google.gwt.thirdparty.guava.common.collect.Maps;
 
-import com.gargoylesoftware.htmlunit.AlertHandler;
-import com.gargoylesoftware.htmlunit.BrowserVersion;
-import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
-import com.gargoylesoftware.htmlunit.IncorrectnessListener;
-import com.gargoylesoftware.htmlunit.OnbeforeunloadHandler;
-import com.gargoylesoftware.htmlunit.Page;
-import com.gargoylesoftware.htmlunit.ScriptException;
-import com.gargoylesoftware.htmlunit.WebClient;
-import com.gargoylesoftware.htmlunit.WebWindow;
-import com.gargoylesoftware.htmlunit.html.HtmlPage;
-import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
-import com.gargoylesoftware.htmlunit.javascript.JavaScriptErrorListener;
-import com.gargoylesoftware.htmlunit.javascript.host.Window;
-import com.gargoylesoftware.htmlunit.util.WebClientUtils;
-
-import net.sourceforge.htmlunit.corejs.javascript.Context;
-import net.sourceforge.htmlunit.corejs.javascript.Function;
-import net.sourceforge.htmlunit.corejs.javascript.JavaScriptException;
-import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
+import org.htmlunit.AlertHandler;
+import org.htmlunit.BrowserVersion;
+import org.htmlunit.FailingHttpStatusCodeException;
+import org.htmlunit.IncorrectnessListener;
+import org.htmlunit.OnbeforeunloadHandler;
+import org.htmlunit.Page;
+import org.htmlunit.ScriptException;
+import org.htmlunit.WebClient;
+import org.htmlunit.WebWindow;
+import org.htmlunit.corejs.javascript.Context;
+import org.htmlunit.corejs.javascript.ScriptableObject;
+import org.htmlunit.html.HtmlPage;
+import org.htmlunit.javascript.HtmlUnitContextFactory;
+import org.htmlunit.javascript.JavaScriptEngine;
+import org.htmlunit.javascript.JavaScriptErrorListener;
+import org.htmlunit.javascript.host.Window;
+import org.htmlunit.util.WebClientUtils;
 
 import java.io.IOException;
+import java.lang.reflect.Field;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
@@ -52,6 +48,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TimeZone;
 
 /**
  * Launches a web-mode test via HTMLUnit.
@@ -105,6 +102,7 @@ public void run() {
       webClient.setAlertHandler(this);
       webClient.setIncorrectnessListener(this);
       webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
+      webClient.getOptions().setFetchPolyfillEnabled(true);
       // To receive exceptions from js side in the development mode, we need set this to 'true'.
       // However, as htmlunit dies after throwing the exception, we still want it to be 'false'
       // for web mode.
@@ -138,7 +136,7 @@ public void malformedScriptURL(HtmlPage htmlPage, String url,
         @Override
         public void scriptException(HtmlPage htmlPage,
             ScriptException scriptException) {
-          treeLogger.log(TreeLogger.DEBUG,
+          treeLogger.log(TreeLogger.ERROR,
               "Script Exception: " + scriptException.getLocalizedMessage() +
                ", line " + scriptException.getFailingLine());
         }
@@ -177,7 +175,7 @@ protected void setupWebClient(WebClient webClient) {
             treeLogger);
         webClient.setJavaScriptEngine(hostedEngine);
       } else {
-        JavaScriptEngine webEngine = new WebJavaScriptEngine(webClient);
+        JavaScriptEngine webEngine = new JavaScriptEngine(webClient);
         webClient.setJavaScriptEngine(webEngine);
       }
       if (System.getProperty("gwt.htmlunit.debug") != null) {
@@ -196,108 +194,57 @@ private static class HostedJavaScriptEngine extends JavaScriptEngine {
     private static final long serialVersionUID = 3594816610842448691L;
     private final WebClient webClient;
     private final TreeLogger logger;
+    private final HtmlUnitContextFactory htmlUnitContextFactory;
 
-    public HostedJavaScriptEngine(WebClient webClient, TreeLogger logger) {
+    HostedJavaScriptEngine(WebClient webClient, TreeLogger logger) {
       super(webClient);
       this.webClient = webClient;
       this.logger = logger;
+      this.htmlUnitContextFactory = new HtmlUnitContextFactory(webClient) {
+        protected Context makeContext() {
+          Context ctx = super.makeContext();
+          try {
+            Field hasShutter = Context.class.getDeclaredField("hasClassShutter");
+            hasShutter.setAccessible(true);
+            hasShutter.setBoolean(ctx, false);
+          } catch (IllegalAccessException | NoSuchFieldException e) {
+            throw new RuntimeException(e);
+          }
+          ctx.setClassShutter(any -> true);
+          return ctx;
+        }
+      };
+    }
+
+    @Override
+    public HtmlUnitContextFactory getContextFactory() {
+      return htmlUnitContextFactory;
     }
 
     @Override
     public void initialize(WebWindow webWindow, Page page) {
       // Hook in the hosted-mode plugin after initializing the JS engine.
       super.initialize(webWindow, page);
-      Window window = (Window) webWindow.getScriptableObject();
+      Window window = webWindow.getScriptableObject();
       window.defineProperty("__gwt_HostedModePlugin",
           new HostedModePluginObject(this, webClient, logger), ScriptableObject.READONLY);
     }
   }
 
-  /**
-   * JavaScriptEngine subclass that fixes a bug when calling {@code window.onerror}.
-   * Make sure to remove when updating HtmlUnit.
-   *
-   * @see HtmlUnit bug #1924
-   */
-  private static class WebJavaScriptEngine extends JavaScriptEngine {
-    private static final Log LOG = LogFactory.getLog(JavaScriptEngine.class);
-    private final WebClient webClient;
-
-    public WebJavaScriptEngine(WebClient webClient) {
-      super(webClient);
-      this.webClient = webClient;
-    }
-
-    @Override
-    protected void handleJavaScriptException(ScriptException scriptException,
-        boolean triggerOnError) {
-      // XXX(tbroyer): copied from JavaScriptEngine to call below triggerOnError
-      // instead of Window's triggerOnError.
-
-      // Trigger window.onerror, if it has been set.
-      final HtmlPage page = scriptException.getPage();
-      if (triggerOnError && page != null) {
-        final WebWindow window = page.getEnclosingWindow();
-        if (window != null) {
-          final Window w = (Window) window.getScriptableObject();
-          if (w != null) {
-            try {
-              triggerOnError(w, scriptException);
-            } catch (final Exception e) {
-              handleJavaScriptException(new ScriptException(page, e, null), false);
-            }
-          }
-        }
-      }
-      final JavaScriptErrorListener javaScriptErrorListener =
-              webClient.getJavaScriptErrorListener();
-      if (javaScriptErrorListener != null) {
-        javaScriptErrorListener.scriptException(page, scriptException);
-      }
-      // Throw a Java exception if the user wants us to.
-      if (webClient.getOptions().isThrowExceptionOnScriptError()) {
-        throw scriptException;
-      }
-      // Log the error; ScriptException instances provide good debug info.
-      LOG.info("Caught script exception", scriptException);
-    }
-
-    private void triggerOnError(Window w, ScriptException e) {
-      // XXX(tbroyer): copied from HtmlUnit's javascript.host.Window
-      // with fix unwrapping the JS exception before passing it back to JS.
-      final Object o = w.getOnerror();
-      if (o instanceof Function) {
-        final Function f = (Function) o;
-        final String msg = e.getMessage();
-        final String url = e.getPage().getUrl().toExternalForm();
-        final int line = e.getFailingLineNumber();
-
-        final int column = e.getFailingColumnNumber();
-
-        Object jsError = null;
-        if (e.getCause() instanceof JavaScriptException) {
-          jsError = ((JavaScriptException) e.getCause()).getValue();
-        }
-
-        Object[] args = new Object[]{msg, url, line, column, jsError};
-
-        f.call(Context.getCurrentContext(), w, w, args);
-      }
-    }
-  }
-
   private static final Map BROWSER_MAP = Maps.newHashMap();
   private static final Map USER_AGENT_MAP  = Maps.newHashMap();
 
   static {
-    // “Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36 Edge/12.0″
+    // “Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)
+    // Chrome/39.0.2171.71 Safari/537.36 Edge/12.0″
     addBrowser(BrowserVersion.EDGE, "safari");
     addBrowser(BrowserVersion.FIREFOX, "gecko1_8");
     addBrowser(BrowserVersion.CHROME, "safari");
-    addBrowser(BrowserVersion.INTERNET_EXPLORER, "gecko1_8");
   }
 
-  private static void addBrowser(BrowserVersion browser, String userAgent) {
+  private static void addBrowser(BrowserVersion baseBrowser, String userAgent) {
+    BrowserVersion browser = new BrowserVersion.BrowserVersionBuilder(baseBrowser)
+        .setSystemTimezone(TimeZone.getDefault()).build();
     BROWSER_MAP.put(browser.getNickname(), browser);
     USER_AGENT_MAP.put(browser, userAgent);
   }
diff --git a/user/test/com/google/gwt/emultest/java/util/DateTest.java b/user/test/com/google/gwt/emultest/java/util/DateTest.java
index 452e1f53f9..ba1507c0df 100644
--- a/user/test/com/google/gwt/emultest/java/util/DateTest.java
+++ b/user/test/com/google/gwt/emultest/java/util/DateTest.java
@@ -571,15 +571,17 @@ public void testToLocaleString() {
       // /////////////////////////////
       // Past
       // /////////////////////////////
-      Date accum1 = create(PAST);
+      Date accum1 = createLocal("1/5/1890");
       String a1 = accum1.toLocaleString();
-      assertTrue(a1.indexOf("1890") != -1);
+      assertTrue(a1 + " should describe 1/5/1890",
+          a1.contains("1890") || a1.contains("1/5/90"));
       // /////////////////////////////
       // Future
       // /////////////////////////////
-      Date accum2 = create(FUTURE);
+      Date accum2 = createLocal("12/30/2030 3:4:5");
       String a2 = accum2.toLocaleString();
-      assertTrue(a2.indexOf("2030") != -1);
+      assertTrue(a2 + " should describe 12/30/2030",
+          a2.contains("2030") || a2.contains("12/30/30"));
     }
   }
 
@@ -895,6 +897,11 @@ Date create(String s) {
     }
   }
 
+  Date createLocal(String s) {
+     // aligned with the TZ used in CI
+     return new Date(s + " GMT-8:00");
+  }
+
   private String createString(String s) {
     if (s.equals(FUTURE)) {
       return "12/30/2030 3:4:5 GMT";