From 8f94b4f98e646fbe513a105d1d3c09ca33cb6dfe Mon Sep 17 00:00:00 2001 From: Tomi Virtanen Date: Mon, 30 Mar 2026 12:00:15 +0300 Subject: [PATCH 1/2] chore: enhance HarFilter and HarToK6Converter to filter unsupported HTTP methods and WebSocket upgrades --- .../testbench/loadtest/util/HarFilter.java | 41 ++++++ .../loadtest/util/HarToK6Converter.java | 23 +++- .../loadtest/util/HarFilterTest.java | 126 ++++++++++++++++++ .../loadtest/util/HarToK6ConverterTest.java | 83 ++++++++++++ 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarFilterTest.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarToK6ConverterTest.java diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarFilter.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarFilter.java index 74f4f36d9..b63336c51 100644 --- a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarFilter.java +++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarFilter.java @@ -14,6 +14,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.logging.Logger; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -44,6 +45,13 @@ public class HarFilter { // Common CDNs and analytics (usually not part of app testing) "cloudflare.com", "akamai.net", "fastly.net"); + /** + * HTTP methods supported by k6's http module. Entries with other methods + * (e.g., CONNECT for HTTPS tunneling) are filtered out. + */ + private static final Set K6_SUPPORTED_METHODS = Set.of("GET", + "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"); + private static final Logger log = Logger .getLogger(HarFilter.class.getName()); private final ObjectMapper objectMapper; @@ -109,6 +117,19 @@ public FilterResult filter(Path inputFile, Path outputFile) log.fine(" Filtered UNLOAD request: " + url); continue; } + // Filter unsupported HTTP methods (CONNECT for HTTPS + // tunneling, TRACE, etc.) + if (entry.request().method() != null && !K6_SUPPORTED_METHODS + .contains(entry.request().method().toUpperCase())) { + log.fine(" Filtered unsupported method " + + entry.request().method() + ": " + url); + continue; + } + // Filter WebSocket upgrade requests (not replayable as HTTP) + if (isWebSocketUpgrade(entry)) { + log.fine(" Filtered WebSocket upgrade: " + url); + continue; + } } filteredEntries.add(entry); } @@ -147,6 +168,26 @@ private boolean isUnloadRequest(HarEntry entry) { return false; } + /** + * Check if a request is a WebSocket upgrade (GET with {@code Upgrade: + * websocket} header). These cannot be replayed as regular HTTP requests. + * + * @param entry + * the HAR entry to check + * @return {@code true} if the entry is a WebSocket upgrade request + */ + private boolean isWebSocketUpgrade(HarEntry entry) { + if (entry.request().headers() != null) { + for (HarHeader header : entry.request().headers()) { + if ("upgrade".equalsIgnoreCase(header.name()) + && "websocket".equalsIgnoreCase(header.value())) { + return true; + } + } + } + return false; + } + /** * Check if a URL belongs to an external domain that should be filtered. * diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java index b9f279297..325443fa7 100644 --- a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java +++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java @@ -14,6 +14,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -30,6 +31,13 @@ */ public class HarToK6Converter { + /** + * HTTP methods supported by k6's http module. Entries with other methods + * (e.g., CONNECT for HTTPS tunneling) are skipped during conversion. + */ + private static final Set K6_SUPPORTED_METHODS = Set.of("GET", + "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"); + private static final Logger log = Logger .getLogger(HarToK6Converter.class.getName()); private static final Pattern SYNC_CLIENT_ID_PATTERN = Pattern @@ -138,6 +146,16 @@ public void convert(Path harFile, Path outputFile, for (int i = 0; i < entries.size(); i++) { HarEntry entry = entries.get(i); + + // Skip unsupported HTTP methods (safety net for unfiltered HAR + // files) + String method = entry.request().method().toUpperCase(); + if (!K6_SUPPORTED_METHODS.contains(method)) { + log.warning(" Skipping unsupported HTTP method: " + method + + " " + truncateUrl(entry.request().url())); + continue; + } + // Calculate time delta from previous request long deltaMs = -1; if (i > 0) { @@ -315,8 +333,11 @@ private String generateRequestCode(HarEntry entry, int index, long deltaMs, " selectedKey = gridKeys.length > 0 ? gridKeys[Math.floor(Math.random() * gridKeys.length)] : '0'\n"); } + // k6 uses http.del() for DELETE, not http.delete() + String k6Method = "DELETE".equals(method) ? "del" + : method.toLowerCase(); code.append(" ").append(responseDecl).append(" = http.") - .append(method.toLowerCase()).append("(\n"); + .append(k6Method).append("(\n"); if (dynamicUrl) { String escapedUrl = UI_ID_URL_PATTERN diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarFilterTest.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarFilterTest.java new file mode 100644 index 000000000..690773ce3 --- /dev/null +++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarFilterTest.java @@ -0,0 +1,126 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +package com.vaadin.testbench.loadtest.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class HarFilterTest { + + @TempDir + Path tempDir; + + @Test + void connectRequestsAreFiltered() throws IOException { + String har = createHarWithEntries( + entry("CONNECT", "http://localhost:8080", null), + entry("GET", "http://localhost:8080/", null)); + + Path harFile = tempDir.resolve("test.har"); + Files.writeString(harFile, har); + + HarFilter filter = new HarFilter(); + HarFilter.FilterResult result = filter.filter(harFile); + + assertEquals(2, result.originalCount()); + assertEquals(1, result.filteredCount()); + assertEquals(1, result.remainingCount()); + } + + @Test + void traceRequestsAreFiltered() throws IOException { + String har = createHarWithEntries( + entry("TRACE", "http://localhost:8080/", null), + entry("GET", "http://localhost:8080/", null)); + + Path harFile = tempDir.resolve("test.har"); + Files.writeString(harFile, har); + + HarFilter filter = new HarFilter(); + HarFilter.FilterResult result = filter.filter(harFile); + + assertEquals(1, result.filteredCount()); + assertEquals(1, result.remainingCount()); + } + + @Test + void webSocketUpgradeRequestsAreFiltered() throws IOException { + String har = createHarWithEntries( + entry("GET", "http://localhost:8080/PUSH", + List.of(header("Upgrade", "websocket"), + header("Connection", "Upgrade"))), + entry("GET", "http://localhost:8080/", null)); + + Path harFile = tempDir.resolve("test.har"); + Files.writeString(harFile, har); + + HarFilter filter = new HarFilter(); + HarFilter.FilterResult result = filter.filter(harFile); + + assertEquals(1, result.filteredCount()); + assertEquals(1, result.remainingCount()); + } + + @Test + void supportedMethodsAreKept() throws IOException { + String har = createHarWithEntries( + entry("GET", "http://localhost:8080/", null), + entry("POST", "http://localhost:8080/api", null), + entry("PUT", "http://localhost:8080/api/1", null), + entry("PATCH", "http://localhost:8080/api/1", null), + entry("DELETE", "http://localhost:8080/api/1", null), + entry("HEAD", "http://localhost:8080/", null), + entry("OPTIONS", "http://localhost:8080/api", null)); + + Path harFile = tempDir.resolve("test.har"); + Files.writeString(harFile, har); + + HarFilter filter = new HarFilter(); + HarFilter.FilterResult result = filter.filter(harFile); + + assertEquals(0, result.filteredCount()); + assertEquals(7, result.remainingCount()); + } + + // --- Helper methods to build HAR JSON --- + + private String header(String name, String value) { + return "{\"name\":\"" + name + "\",\"value\":\"" + value + "\"}"; + } + + private String entry(String method, String url, List headerJsons) { + String headers = headerJsons != null + ? "[" + String.join(",", headerJsons) + "]" + : "[]"; + return """ + {"startedDateTime":"2026-01-01T00:00:00.000Z","time":0,\ + "request":{"method":"%s","url":"%s","httpVersion":"HTTP/1.1",\ + "headers":%s,"queryString":[],"cookies":[],"headersSize":-1,\ + "bodySize":-1},\ + "response":{"status":200,"statusText":"OK","httpVersion":"HTTP/1.1",\ + "headers":[],"cookies":[],"content":{"size":0,"mimeType":"text/html"},\ + "redirectURL":"","headersSize":-1,"bodySize":-1},\ + "cache":{},"timings":{"blocked":0,"dns":0,"connect":0,"send":0,"wait":0,"receive":0}}""" + .formatted(method, url, headers); + } + + private String createHarWithEntries(String... entries) { + return """ + {"log":{"version":"1.2","creator":{"name":"test","version":"1.0"},\ + "entries":[%s],"pages":[]}}""" + .formatted(String.join(",", entries)); + } +} diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarToK6ConverterTest.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarToK6ConverterTest.java new file mode 100644 index 000000000..9bc99ff96 --- /dev/null +++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarToK6ConverterTest.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +package com.vaadin.testbench.loadtest.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HarToK6ConverterTest { + + @TempDir + Path tempDir; + + @Test + void connectMethodIsSkipped() throws IOException { + String har = createHar(entry("CONNECT", "http://localhost:8080"), + entry("GET", "http://localhost:8080/")); + + Path harFile = tempDir.resolve("test.har"); + Path outputFile = tempDir.resolve("test.js"); + Files.writeString(harFile, har); + + new HarToK6Converter().convert(harFile, outputFile); + + String script = Files.readString(outputFile); + assertFalse(script.contains("http.connect("), + "Generated script should not contain http.connect()"); + assertTrue(script.contains("http.get("), + "Generated script should contain http.get()"); + } + + @Test + void deleteMethodUsesDelFunction() throws IOException { + String har = createHar(entry("GET", "http://localhost:8080/"), + entry("DELETE", "http://localhost:8080/api/1")); + + Path harFile = tempDir.resolve("test.har"); + Path outputFile = tempDir.resolve("test.js"); + Files.writeString(harFile, har); + + new HarToK6Converter().convert(harFile, outputFile); + + String script = Files.readString(outputFile); + assertTrue(script.contains("http.del("), + "Generated script should use http.del() for DELETE"); + assertFalse(script.contains("http.delete("), + "Generated script should not contain http.delete()"); + } + + // --- Helper methods to build HAR JSON --- + + private String entry(String method, String url) { + return """ + {"startedDateTime":"2026-01-01T00:00:00.000Z","time":0,\ + "request":{"method":"%s","url":"%s","httpVersion":"HTTP/1.1",\ + "headers":[],"queryString":[],"cookies":[],"headersSize":-1,\ + "bodySize":-1},\ + "response":{"status":200,"statusText":"OK","httpVersion":"HTTP/1.1",\ + "headers":[],"cookies":[],"content":{"size":0,"mimeType":"text/html"},\ + "redirectURL":"","headersSize":-1,"bodySize":-1},\ + "cache":{},"timings":{"blocked":0,"dns":0,"connect":0,"send":0,"wait":0,"receive":0}}""" + .formatted(method, url); + } + + private String createHar(String... entries) { + return """ + {"log":{"version":"1.2","creator":{"name":"test","version":"1.0"},\ + "entries":[%s],"pages":[]}}""" + .formatted(String.join(",", entries)); + } +} From 9897d6285fc1404accefb6ca5288ac0bd63bba44 Mon Sep 17 00:00:00 2001 From: Tomi Virtanen Date: Tue, 31 Mar 2026 10:29:03 +0300 Subject: [PATCH 2/2] Put supported HTTP methods in one place --- .../com/vaadin/testbench/loadtest/util/HarFilter.java | 4 ++-- .../testbench/loadtest/util/HarToK6Converter.java | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarFilter.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarFilter.java index b63336c51..32df197f2 100644 --- a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarFilter.java +++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarFilter.java @@ -49,8 +49,8 @@ public class HarFilter { * HTTP methods supported by k6's http module. Entries with other methods * (e.g., CONNECT for HTTPS tunneling) are filtered out. */ - private static final Set K6_SUPPORTED_METHODS = Set.of("GET", - "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"); + static final Set K6_SUPPORTED_METHODS = Set.of("GET", "POST", "PUT", + "PATCH", "DELETE", "HEAD", "OPTIONS"); private static final Logger log = Logger .getLogger(HarFilter.class.getName()); diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java index 325443fa7..53dd622d2 100644 --- a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java +++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java @@ -14,7 +14,6 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; -import java.util.Set; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -31,13 +30,6 @@ */ public class HarToK6Converter { - /** - * HTTP methods supported by k6's http module. Entries with other methods - * (e.g., CONNECT for HTTPS tunneling) are skipped during conversion. - */ - private static final Set K6_SUPPORTED_METHODS = Set.of("GET", - "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"); - private static final Logger log = Logger .getLogger(HarToK6Converter.class.getName()); private static final Pattern SYNC_CLIENT_ID_PATTERN = Pattern @@ -150,7 +142,7 @@ public void convert(Path harFile, Path outputFile, // Skip unsupported HTTP methods (safety net for unfiltered HAR // files) String method = entry.request().method().toUpperCase(); - if (!K6_SUPPORTED_METHODS.contains(method)) { + if (!HarFilter.K6_SUPPORTED_METHODS.contains(method)) { log.warning(" Skipping unsupported HTTP method: " + method + " " + truncateUrl(entry.request().url())); continue;