How can I set the encoding of a JavaFX WebView?

WebView determines the encoding from either the HTML file or the HTTP header. This is as per the w3c specification, for information see:

  • Declaring character encodings in HTML

As you already noted in your question, you can declare the character encoding in the head element within the HTML document and the WebView will pick it up:

<!DOCTYPE html>
<html lang="en"> 
<head>
<meta charset="utf-8"/>
...

But, you also note in your question that you don't have control over the input HTML files and whether it includes the necessary header for declaring the charset.

You can also have the HTTP protocol specify the encoding of the file using an appropriate header.

 Content-Type: text/html; charset=UTF-8

If you do that, the HTML file content will be correctly UTF-8 decoded by the WebView, even if the input file does not include a charset header.

Here is an example:

import com.sun.net.httpserver.*;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;

public class WebViewTest extends Application {

    private static final String TEST_HTML = "test.html";

    private HttpServer server;

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void init() throws Exception {
        server = HttpServer.create(new InetSocketAddress(8000), 0);
        server.createContext("/", new MyHandler());
        server.setExecutor(null); // creates a default executor
        server.start();
    }

    @Override
    public void start(Stage stage) {
        WebView webView = new WebView();
        webView.getEngine().load("http://localhost:8000/" + TEST_HTML);

        Scene scene = new Scene(webView, 500, 500);
        stage.setScene(scene);
        stage.setTitle("WebView Test");
        stage.show();
    }

    @Override
    public void stop() throws Exception {
        server.stop(0);
    }

    static class MyHandler implements HttpHandler {
        public void handle(HttpExchange httpExchange) {
            try {
                String path = httpExchange.getRequestURI().getPath().substring(1);  // strips leading slash from path, so resource lookup will be relative to this class, not the root.
                String testString = resourceAsString(path);
                System.out.println("testString = " + testString);
                if (testString != null) {
                    httpExchange.getResponseHeaders().put("Content-Type", List.of("text/html; charset=UTF-8"));
                    httpExchange.sendResponseHeaders(200, testString.getBytes(StandardCharsets.UTF_8).length);
                    try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(httpExchange.getResponseBody()))) {
                        writer.write(testString);
                        writer.flush();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("Unable to find resource: " + path);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        private String resourceAsString(String fileName) throws IOException {
            try (InputStream is = WebViewTest.class.getResourceAsStream(fileName)) {
                if (is == null) return null;
                try (InputStreamReader isr = new InputStreamReader(is);
                     BufferedReader reader = new BufferedReader(isr)) {
                    return reader.lines().collect(Collectors.joining(System.lineSeparator()));
                }
            }
        }
    }
}

For this example to work, place the HTML test file from your question in the same location as your compiled WebViewTest.class, so that it can be loaded from there as a resource.

To run the example as a modular app, add the following to your module-info.java (in addition to your javafx module requirements and any other app requirements you need):

requires jdk.httpserver;

While writing the question, I found a hacky solution:

webView.getEngine().getLoadWorker().stateProperty().addListener((o, oldState, newState) -> {
    if(newState == Worker.State.SUCCEEDED) {
        try {
            String newContent = new String(Files.readAllBytes(Paths.get(new URI(getClass().getResource("/test.html").toExternalForm()))), "UTF-8");
            webView.getEngine().executeScript("document.documentElement.innerHTML = '" + newContent.replace("'", "\\'").replace("\n", "\\n") + "'");
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
});

Correctly displayed result