fix(e2e): retry onion API requests past the 30s socks connect cap

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.8 committed 2026-07-19 11:29:14 +02:00
1 parent 78a0807039
commit 0a52433dcb
2 files changed
+33 -2

No files matched your search

@@ -72,6 +72,36 @@ async function gotoOnion(page, url, opts = {}) {
throw lastErr;
}
/**
* Tor-resilient `request.get` for the standalone APIRequestContext fixture.
* Its SOCKS CONNECT goes through the bundled `socks` client whose 30s connect
* cap is not configurable, so a cold onion circuit fails the request no matter
* how large the request timeout is. Retries only transient proxy/socket
* errors; clearnet URLs get a single attempt and real HTTP failures re-throw.
*/
const _API_TRANSIENT_RE =
/Proxy connection timed out|Socket closed|socket hang up|ECONNRESET|ETIMEDOUT|ECONNREFUSED|ENOTFOUND/i;
async function apiGetOnion(request, url, opts = {}) {
const isRelative = /^\/(?!\/)/.test(url);
const isOnion =
/\.onion(?::\d+)?(?:\/|$|\?)/i.test(url) || (isRelative && isOnionCanonical());
const attempts = isOnion ? Number(process.env.PLAYWRIGHT_ONION_GOTO_RETRIES) || 4 : 1;
let lastErr;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await request.get(url, opts);
} catch (err) {
lastErr = err;
if (attempt >= attempts || !_API_TRANSIENT_RE.test(String(err && err.message))) {
throw err;
}
await new Promise((resolve) => setTimeout(resolve, resolveTimeout(2_000 * attempt)));
}
}
throw lastErr;
}
/**
* Tolerant variant of `skipUnlessServiceEnabled`: treats an unknown
* service (i.e. one whose `<NAME>_SERVICE_ENABLED` flag is not declared
@@ -106,6 +136,7 @@ module.exports = {
readEnv,
isOnionCanonical,
gotoOnion,
apiGetOnion,
safeSkipUnlessEnabled,
safeIsEnabled,
};
@@ -1,7 +1,7 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, gotoOnion, inAppLogout, performKeycloakLoginForm, runAdminFlow, runBiberFlow, runGuestFlow, safeSkipUnlessEnabled } = require("./personas");
const { apiGetOnion, decodeDotenvQuotedValue, gotoOnion, inAppLogout, performKeycloakLoginForm, runAdminFlow, runBiberFlow, runGuestFlow, safeSkipUnlessEnabled } = require("./personas");
test.use({
ignoreHTTPSErrors: true
});
@@ -40,7 +40,7 @@ test.beforeEach(() => {
// used to live in each consumer role's own playwright.spec.js.
test("metricz endpoint is accessible and returns prometheus text format", async ({ request }) => {
const metriczUrl = `${prometheusBaseUrl.replace(/\/$/, "")}/metricz`;
const response = await request.get(metriczUrl, { timeout: resolveTimeout(30_000) });
const response = await apiGetOnion(request, metriczUrl, { timeout: resolveTimeout(30_000) });
expect(
response.status(),