Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*/
static final Set<String> 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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,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 (!HarFilter.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) {
Expand Down Expand Up @@ -315,8 +325,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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* Copyright (C) 2000-2026 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See <https://vaadin.com/commercial-license-and-service-terms> 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<String> 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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Copyright (C) 2000-2026 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See <https://vaadin.com/commercial-license-and-service-terms> 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));
}
}
Loading