norn-cli 1.2.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/cli.js +240 -125
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the "Norn" extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.0] - 2026-02-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Retry and Backoff**: Automatically retry failed HTTP requests with configurable backoff
|
|
9
|
+
- Syntax: `GET url retry 3 backoff 200 ms` (retry up to 3 times with 200ms linear backoff)
|
|
10
|
+
- Works with direct URLs, named endpoints, and named requests
|
|
11
|
+
- Supports time units: `ms`, `s`, `seconds`, `milliseconds`
|
|
12
|
+
- Response panel shows retry indicator: 🔄 `retried 2x`
|
|
13
|
+
- CLI shows retry attempts in real-time
|
|
14
|
+
- Retries on 5xx errors, 429 rate limiting, and network failures
|
|
15
|
+
|
|
16
|
+
### Improved
|
|
17
|
+
- **Syntax highlighting**: Variable references `{{var}}` now highlighted inside print strings
|
|
18
|
+
- **Better pattern recognition**: `test sequence` correctly terminates request blocks in all contexts
|
|
19
|
+
|
|
5
20
|
## [1.2.5] - 2026-02-07
|
|
6
21
|
|
|
7
22
|
### Improved
|
package/dist/cli.js
CHANGED
|
@@ -13286,6 +13286,31 @@ function stripInlineComment(line2) {
|
|
|
13286
13286
|
}
|
|
13287
13287
|
return line2;
|
|
13288
13288
|
}
|
|
13289
|
+
function extractRetryOptions(line2) {
|
|
13290
|
+
let cleanedLine = line2;
|
|
13291
|
+
let retryCount;
|
|
13292
|
+
let backoffMs;
|
|
13293
|
+
const retryMatch = line2.match(/\bretry\s+(\d+)\b/i);
|
|
13294
|
+
if (retryMatch) {
|
|
13295
|
+
retryCount = parseInt(retryMatch[1], 10);
|
|
13296
|
+
cleanedLine = cleanedLine.replace(retryMatch[0], "").trim();
|
|
13297
|
+
}
|
|
13298
|
+
const backoffMatch = line2.match(/\bbackoff\s+(\d+(?:\.\d+)?)\s*(s|ms|seconds?|milliseconds?)?\b/i);
|
|
13299
|
+
if (backoffMatch) {
|
|
13300
|
+
const value = parseFloat(backoffMatch[1]);
|
|
13301
|
+
const unit = (backoffMatch[2] || "ms").toLowerCase();
|
|
13302
|
+
if (unit === "s" || unit.startsWith("second")) {
|
|
13303
|
+
backoffMs = value * 1e3;
|
|
13304
|
+
} else {
|
|
13305
|
+
backoffMs = value;
|
|
13306
|
+
}
|
|
13307
|
+
cleanedLine = cleanedLine.replace(backoffMatch[0], "").trim();
|
|
13308
|
+
}
|
|
13309
|
+
if (retryCount !== void 0 && backoffMs === void 0) {
|
|
13310
|
+
backoffMs = 1e3;
|
|
13311
|
+
}
|
|
13312
|
+
return { cleanedLine, retryCount, backoffMs };
|
|
13313
|
+
}
|
|
13289
13314
|
function extractNamedRequests(text) {
|
|
13290
13315
|
const lines = text.split("\n");
|
|
13291
13316
|
const namedRequests = [];
|
|
@@ -13441,7 +13466,9 @@ function parserHttpRequest(text, variables = {}) {
|
|
|
13441
13466
|
if (requestLineIndex === -1) {
|
|
13442
13467
|
throw new Error("No valid HTTP method found");
|
|
13443
13468
|
}
|
|
13444
|
-
|
|
13469
|
+
let requestLine = stripInlineComment(allLines[requestLineIndex].trim());
|
|
13470
|
+
const { cleanedLine, retryCount, backoffMs } = extractRetryOptions(requestLine);
|
|
13471
|
+
requestLine = cleanedLine;
|
|
13445
13472
|
const [method, ...urlParts] = requestLine.split(" ");
|
|
13446
13473
|
let url2 = urlParts.join(" ");
|
|
13447
13474
|
if (url2.startsWith('"') && url2.endsWith('"') || url2.startsWith("'") && url2.endsWith("'")) {
|
|
@@ -13490,7 +13517,7 @@ function parserHttpRequest(text, variables = {}) {
|
|
|
13490
13517
|
body = void 0;
|
|
13491
13518
|
}
|
|
13492
13519
|
}
|
|
13493
|
-
return { method: method.toUpperCase(), url: url2, headers, body };
|
|
13520
|
+
return { method: method.toUpperCase(), url: url2, headers, body, retryCount, backoffMs };
|
|
13494
13521
|
}
|
|
13495
13522
|
function extractImports(text) {
|
|
13496
13523
|
const lines = text.split("\n");
|
|
@@ -19463,114 +19490,157 @@ async function getAllCookies(jar) {
|
|
|
19463
19490
|
secure: Boolean(c.secure)
|
|
19464
19491
|
}));
|
|
19465
19492
|
}
|
|
19466
|
-
async function sendRequest(request) {
|
|
19467
|
-
return sendRequestWithJar(request, sharedCookieJar);
|
|
19493
|
+
async function sendRequest(request, retryOptions) {
|
|
19494
|
+
return sendRequestWithJar(request, sharedCookieJar, retryOptions);
|
|
19468
19495
|
}
|
|
19469
|
-
|
|
19470
|
-
|
|
19471
|
-
|
|
19472
|
-
|
|
19473
|
-
|
|
19474
|
-
|
|
19475
|
-
|
|
19476
|
-
|
|
19477
|
-
|
|
19478
|
-
|
|
19479
|
-
|
|
19480
|
-
|
|
19481
|
-
|
|
19482
|
-
|
|
19483
|
-
|
|
19484
|
-
|
|
19485
|
-
|
|
19486
|
-
|
|
19487
|
-
|
|
19488
|
-
|
|
19489
|
-
|
|
19490
|
-
|
|
19491
|
-
|
|
19492
|
-
|
|
19493
|
-
|
|
19494
|
-
|
|
19495
|
-
|
|
19496
|
-
|
|
19497
|
-
|
|
19498
|
-
|
|
19496
|
+
function sleep(ms) {
|
|
19497
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
19498
|
+
}
|
|
19499
|
+
function shouldRetry(response, error) {
|
|
19500
|
+
if (error) return true;
|
|
19501
|
+
if (response && (response.status >= 500 || response.status === 429)) return true;
|
|
19502
|
+
return false;
|
|
19503
|
+
}
|
|
19504
|
+
async function sendRequestWithJar(request, jar, retryOptions) {
|
|
19505
|
+
const totalAttempts = (retryOptions?.retryCount ?? 0) + 1;
|
|
19506
|
+
const backoffMs = retryOptions?.backoffMs ?? 1e3;
|
|
19507
|
+
const retriedErrors = [];
|
|
19508
|
+
let lastError = null;
|
|
19509
|
+
let attemptsMade = 0;
|
|
19510
|
+
for (let attempt = 1; attempt <= totalAttempts; attempt++) {
|
|
19511
|
+
attemptsMade = attempt;
|
|
19512
|
+
const startTime = Date.now();
|
|
19513
|
+
const maxRedirects = 10;
|
|
19514
|
+
let redirectCount = 0;
|
|
19515
|
+
let currentUrl = request.url;
|
|
19516
|
+
let currentMethod = request.method;
|
|
19517
|
+
let currentBody = request.body;
|
|
19518
|
+
let finalResponse = null;
|
|
19519
|
+
try {
|
|
19520
|
+
while (redirectCount <= maxRedirects) {
|
|
19521
|
+
const existingCookies = await jar.getCookies(currentUrl);
|
|
19522
|
+
const cookieHeader = existingCookies.map((c) => `${c.key}=${c.value}`).join("; ");
|
|
19523
|
+
const headers = { ...request.headers };
|
|
19524
|
+
if (cookieHeader) {
|
|
19525
|
+
headers["Cookie"] = cookieHeader;
|
|
19526
|
+
}
|
|
19527
|
+
let data = void 0;
|
|
19528
|
+
if (currentBody) {
|
|
19529
|
+
const contentType = Object.keys(headers).find(
|
|
19530
|
+
(key) => key.toLowerCase() === "content-type"
|
|
19531
|
+
);
|
|
19532
|
+
const contentTypeValue = contentType ? headers[contentType] : "";
|
|
19533
|
+
if (contentTypeValue.includes("application/x-www-form-urlencoded")) {
|
|
19534
|
+
const lines = currentBody.split("\n").map((line2) => line2.trim()).filter((line2) => line2);
|
|
19535
|
+
const params = lines.map((line2) => {
|
|
19536
|
+
if (line2.includes("=")) {
|
|
19537
|
+
const eqIndex = line2.indexOf("=");
|
|
19538
|
+
const key = line2.substring(0, eqIndex).trim();
|
|
19539
|
+
const value = line2.substring(eqIndex + 1).trim();
|
|
19540
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
19541
|
+
}
|
|
19542
|
+
const colonIndex = line2.indexOf(":");
|
|
19543
|
+
if (colonIndex > 0) {
|
|
19544
|
+
const key = line2.substring(0, colonIndex).trim();
|
|
19545
|
+
const value = line2.substring(colonIndex + 1).trim();
|
|
19546
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
19547
|
+
}
|
|
19548
|
+
return encodeURIComponent(line2);
|
|
19549
|
+
});
|
|
19550
|
+
data = params.join("&");
|
|
19551
|
+
} else if (contentTypeValue.includes("application/json")) {
|
|
19552
|
+
try {
|
|
19553
|
+
data = JSON.parse(currentBody);
|
|
19554
|
+
} catch {
|
|
19555
|
+
data = currentBody;
|
|
19499
19556
|
}
|
|
19500
|
-
|
|
19501
|
-
|
|
19502
|
-
|
|
19503
|
-
|
|
19504
|
-
|
|
19557
|
+
} else {
|
|
19558
|
+
try {
|
|
19559
|
+
data = JSON.parse(currentBody);
|
|
19560
|
+
} catch {
|
|
19561
|
+
data = currentBody;
|
|
19505
19562
|
}
|
|
19506
|
-
return encodeURIComponent(line2);
|
|
19507
|
-
});
|
|
19508
|
-
data = params.join("&");
|
|
19509
|
-
} else if (contentTypeValue.includes("application/json")) {
|
|
19510
|
-
try {
|
|
19511
|
-
data = JSON.parse(currentBody);
|
|
19512
|
-
} catch {
|
|
19513
|
-
data = currentBody;
|
|
19514
19563
|
}
|
|
19515
|
-
}
|
|
19516
|
-
|
|
19517
|
-
|
|
19518
|
-
|
|
19519
|
-
|
|
19564
|
+
}
|
|
19565
|
+
const response = await axios_default({
|
|
19566
|
+
method: currentMethod,
|
|
19567
|
+
url: currentUrl,
|
|
19568
|
+
headers,
|
|
19569
|
+
data,
|
|
19570
|
+
timeout: 3e4,
|
|
19571
|
+
maxRedirects: 0,
|
|
19572
|
+
validateStatus: () => true
|
|
19573
|
+
});
|
|
19574
|
+
const setCookieHeader = response.headers["set-cookie"];
|
|
19575
|
+
if (setCookieHeader) {
|
|
19576
|
+
const cookies2 = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
|
19577
|
+
for (const cookieStr of cookies2) {
|
|
19578
|
+
try {
|
|
19579
|
+
await jar.setCookie(cookieStr, currentUrl);
|
|
19580
|
+
} catch {
|
|
19581
|
+
}
|
|
19520
19582
|
}
|
|
19521
19583
|
}
|
|
19522
|
-
|
|
19523
|
-
|
|
19524
|
-
|
|
19525
|
-
|
|
19526
|
-
|
|
19527
|
-
|
|
19528
|
-
|
|
19529
|
-
maxRedirects: 0,
|
|
19530
|
-
// Disable automatic redirects
|
|
19531
|
-
validateStatus: () => true
|
|
19532
|
-
});
|
|
19533
|
-
const setCookieHeader = response.headers["set-cookie"];
|
|
19534
|
-
if (setCookieHeader) {
|
|
19535
|
-
const cookies2 = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
|
19536
|
-
for (const cookieStr of cookies2) {
|
|
19537
|
-
try {
|
|
19538
|
-
await jar.setCookie(cookieStr, currentUrl);
|
|
19539
|
-
} catch {
|
|
19584
|
+
if (response.status >= 300 && response.status < 400 && response.headers["location"]) {
|
|
19585
|
+
redirectCount++;
|
|
19586
|
+
const location = response.headers["location"];
|
|
19587
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
19588
|
+
if (currentMethod !== "GET" && currentMethod !== "HEAD") {
|
|
19589
|
+
currentMethod = "GET";
|
|
19590
|
+
currentBody = void 0;
|
|
19540
19591
|
}
|
|
19592
|
+
continue;
|
|
19541
19593
|
}
|
|
19594
|
+
finalResponse = response;
|
|
19595
|
+
break;
|
|
19542
19596
|
}
|
|
19543
|
-
if (
|
|
19544
|
-
|
|
19545
|
-
|
|
19546
|
-
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19597
|
+
if (!finalResponse) {
|
|
19598
|
+
throw new Error("Too many redirects");
|
|
19599
|
+
}
|
|
19600
|
+
if (shouldRetry(finalResponse, null) && attempt < totalAttempts) {
|
|
19601
|
+
const waitMs = backoffMs * attempt;
|
|
19602
|
+
const errorMessage = `HTTP ${finalResponse.status} ${finalResponse.statusText}`;
|
|
19603
|
+
retriedErrors.push(`Attempt ${attempt}: ${errorMessage}`);
|
|
19604
|
+
if (retryOptions?.onRetry) {
|
|
19605
|
+
retryOptions.onRetry(attempt, totalAttempts, errorMessage, waitMs);
|
|
19550
19606
|
}
|
|
19607
|
+
await sleep(waitMs);
|
|
19551
19608
|
continue;
|
|
19552
19609
|
}
|
|
19553
|
-
|
|
19554
|
-
|
|
19555
|
-
|
|
19556
|
-
|
|
19557
|
-
|
|
19610
|
+
const cookies = await getAllCookies(jar);
|
|
19611
|
+
const result = {
|
|
19612
|
+
status: finalResponse.status,
|
|
19613
|
+
statusText: finalResponse.statusText,
|
|
19614
|
+
headers: finalResponse.headers,
|
|
19615
|
+
body: finalResponse.data,
|
|
19616
|
+
duration: Date.now() - startTime,
|
|
19617
|
+
cookies
|
|
19618
|
+
};
|
|
19619
|
+
if (attemptsMade > 1 || totalAttempts > 1) {
|
|
19620
|
+
result.retryInfo = {
|
|
19621
|
+
attemptsMade,
|
|
19622
|
+
totalAttempts,
|
|
19623
|
+
retriedErrors
|
|
19624
|
+
};
|
|
19625
|
+
}
|
|
19626
|
+
return result;
|
|
19627
|
+
} catch (error) {
|
|
19628
|
+
const errorMessage = error.message || String(error);
|
|
19629
|
+
lastError = new Error(`${errorMessage}
|
|
19630
|
+
URL used: ${currentUrl || request.url}`);
|
|
19631
|
+
if (attempt < totalAttempts) {
|
|
19632
|
+
const waitMs = backoffMs * attempt;
|
|
19633
|
+
retriedErrors.push(`Attempt ${attempt}: ${errorMessage}`);
|
|
19634
|
+
if (retryOptions?.onRetry) {
|
|
19635
|
+
retryOptions.onRetry(attempt, totalAttempts, errorMessage, waitMs);
|
|
19636
|
+
}
|
|
19637
|
+
await sleep(waitMs);
|
|
19638
|
+
continue;
|
|
19639
|
+
}
|
|
19640
|
+
throw lastError;
|
|
19558
19641
|
}
|
|
19559
|
-
const cookies = await getAllCookies(jar);
|
|
19560
|
-
return {
|
|
19561
|
-
status: finalResponse.status,
|
|
19562
|
-
statusText: finalResponse.statusText,
|
|
19563
|
-
headers: finalResponse.headers,
|
|
19564
|
-
body: finalResponse.data,
|
|
19565
|
-
duration: Date.now() - startTime,
|
|
19566
|
-
cookies
|
|
19567
|
-
};
|
|
19568
|
-
} catch (error) {
|
|
19569
|
-
const originalError = error.message || String(error);
|
|
19570
|
-
const urlUsed = currentUrl || request.url;
|
|
19571
|
-
throw new Error(`${originalError}
|
|
19572
|
-
URL used: ${urlUsed}`);
|
|
19573
19642
|
}
|
|
19643
|
+
throw lastError || new Error("Request failed");
|
|
19574
19644
|
}
|
|
19575
19645
|
|
|
19576
19646
|
// src/scriptRunner.ts
|
|
@@ -19955,17 +20025,20 @@ function isRunNamedRequestCommand(line2) {
|
|
|
19955
20025
|
if (/^(bash|js|powershell)\s+/i.test(afterRun)) {
|
|
19956
20026
|
return false;
|
|
19957
20027
|
}
|
|
19958
|
-
return /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))
|
|
20028
|
+
return /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?(?:\s+retry\s+\d+)?(?:\s+backoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?)?$/i.test(afterRun);
|
|
19959
20029
|
}
|
|
19960
20030
|
function parseRunNamedRequestCommand(line2) {
|
|
19961
20031
|
const trimmed = line2.trim();
|
|
19962
|
-
const
|
|
20032
|
+
const { cleanedLine, retryCount, backoffMs } = extractRetryOptions(trimmed);
|
|
20033
|
+
const match = cleanedLine.match(/^run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?$/i);
|
|
19963
20034
|
if (!match) {
|
|
19964
20035
|
return null;
|
|
19965
20036
|
}
|
|
19966
20037
|
return {
|
|
19967
20038
|
name: match[1],
|
|
19968
|
-
args: parseRunArguments(match[2] || "")
|
|
20039
|
+
args: parseRunArguments(match[2] || ""),
|
|
20040
|
+
retryCount,
|
|
20041
|
+
backoffMs
|
|
19969
20042
|
};
|
|
19970
20043
|
}
|
|
19971
20044
|
function isReturnStatement(line2) {
|
|
@@ -20061,7 +20134,7 @@ function parseRunArguments(argsStr) {
|
|
|
20061
20134
|
}
|
|
20062
20135
|
function isVarRunSequenceCommand(line2) {
|
|
20063
20136
|
const trimmed = line2.trim();
|
|
20064
|
-
if (!/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?\s*$/i.test(trimmed)) {
|
|
20137
|
+
if (!/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?(?:\s+retry\s+\d+)?(?:\s+backoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?)?\s*$/i.test(trimmed)) {
|
|
20065
20138
|
return false;
|
|
20066
20139
|
}
|
|
20067
20140
|
const match = trimmed.match(/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+([a-zA-Z_][a-zA-Z0-9_-]*)/i);
|
|
@@ -20075,14 +20148,17 @@ function isVarRunSequenceCommand(line2) {
|
|
|
20075
20148
|
}
|
|
20076
20149
|
function parseVarRunSequenceCommand(line2) {
|
|
20077
20150
|
const trimmed = line2.trim();
|
|
20078
|
-
const
|
|
20151
|
+
const { cleanedLine, retryCount, backoffMs } = extractRetryOptions(trimmed);
|
|
20152
|
+
const match = cleanedLine.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?\s*$/i);
|
|
20079
20153
|
if (!match) {
|
|
20080
20154
|
return null;
|
|
20081
20155
|
}
|
|
20082
20156
|
return {
|
|
20083
20157
|
varName: match[1],
|
|
20084
20158
|
sequenceName: match[2],
|
|
20085
|
-
args: parseRunArguments(match[3] || "")
|
|
20159
|
+
args: parseRunArguments(match[3] || ""),
|
|
20160
|
+
retryCount,
|
|
20161
|
+
backoffMs
|
|
20086
20162
|
};
|
|
20087
20163
|
}
|
|
20088
20164
|
function bindSequenceArguments(params, args, runtimeVariables) {
|
|
@@ -20434,10 +20510,13 @@ function parseVarRequestCommand(line2) {
|
|
|
20434
20510
|
if (!match) {
|
|
20435
20511
|
return null;
|
|
20436
20512
|
}
|
|
20513
|
+
const { cleanedLine: url2, retryCount, backoffMs } = extractRetryOptions(match[3].trim());
|
|
20437
20514
|
return {
|
|
20438
20515
|
varName: match[1],
|
|
20439
20516
|
method: match[2].toUpperCase(),
|
|
20440
|
-
url:
|
|
20517
|
+
url: url2,
|
|
20518
|
+
retryCount,
|
|
20519
|
+
backoffMs
|
|
20441
20520
|
};
|
|
20442
20521
|
}
|
|
20443
20522
|
function isVarAssignCommand(line2) {
|
|
@@ -20668,7 +20747,7 @@ function valueToString2(value) {
|
|
|
20668
20747
|
}
|
|
20669
20748
|
return String(value);
|
|
20670
20749
|
}
|
|
20671
|
-
function
|
|
20750
|
+
function sleep2(ms) {
|
|
20672
20751
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
20673
20752
|
}
|
|
20674
20753
|
function isIfCommand(line2) {
|
|
@@ -21147,7 +21226,7 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21147
21226
|
if (step.type === "wait") {
|
|
21148
21227
|
const durationMs = parseWaitCommand(step.content);
|
|
21149
21228
|
reportProgress(stepIdx, "wait", `wait: ${durationMs}ms`);
|
|
21150
|
-
await
|
|
21229
|
+
await sleep2(durationMs);
|
|
21151
21230
|
const stepResult = {
|
|
21152
21231
|
type: "print",
|
|
21153
21232
|
stepIndex: stepIdx,
|
|
@@ -21388,7 +21467,14 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21388
21467
|
requestText += "\n" + headerLines.join("\n");
|
|
21389
21468
|
}
|
|
21390
21469
|
const requestParsed = parserHttpRequest(requestText, runtimeVariables);
|
|
21391
|
-
const
|
|
21470
|
+
const retryOpts = parsed.retryCount ? {
|
|
21471
|
+
retryCount: parsed.retryCount,
|
|
21472
|
+
backoffMs: parsed.backoffMs || 1e3,
|
|
21473
|
+
onRetry: (attempt, total, error, waitMs) => {
|
|
21474
|
+
reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] ${requestDescription} - waiting ${waitMs}ms`, void 0);
|
|
21475
|
+
}
|
|
21476
|
+
} : void 0;
|
|
21477
|
+
const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar, retryOpts) : await sendRequest(requestParsed, retryOpts);
|
|
21392
21478
|
addResponse(response);
|
|
21393
21479
|
responseIndexToVariable.set(responses.length, parsed.varName);
|
|
21394
21480
|
runtimeVariables[parsed.varName] = response;
|
|
@@ -21641,20 +21727,29 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21641
21727
|
let requestUrl = "";
|
|
21642
21728
|
let requestMethod = "";
|
|
21643
21729
|
try {
|
|
21644
|
-
const
|
|
21645
|
-
requestUrl =
|
|
21646
|
-
requestMethod =
|
|
21647
|
-
const
|
|
21730
|
+
const requestParsed = parserHttpRequest(namedRequest.content, runtimeVariables);
|
|
21731
|
+
requestUrl = requestParsed.url;
|
|
21732
|
+
requestMethod = requestParsed.method;
|
|
21733
|
+
const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
|
|
21734
|
+
const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1e3;
|
|
21735
|
+
const retryOpts = effectiveRetryCount ? {
|
|
21736
|
+
retryCount: effectiveRetryCount,
|
|
21737
|
+
backoffMs: effectiveBackoffMs,
|
|
21738
|
+
onRetry: (attempt, total, error, waitMs) => {
|
|
21739
|
+
reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] run ${targetName} - waiting ${waitMs}ms`, void 0);
|
|
21740
|
+
}
|
|
21741
|
+
} : void 0;
|
|
21742
|
+
const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar, retryOpts) : await sendRequest(requestParsed, retryOpts);
|
|
21648
21743
|
addResponse(response);
|
|
21649
21744
|
const stepResult = {
|
|
21650
21745
|
type: "request",
|
|
21651
21746
|
stepIndex: stepIdx,
|
|
21652
21747
|
response,
|
|
21653
|
-
requestMethod:
|
|
21654
|
-
requestUrl:
|
|
21748
|
+
requestMethod: requestParsed.method,
|
|
21749
|
+
requestUrl: requestParsed.url
|
|
21655
21750
|
};
|
|
21656
21751
|
orderedSteps.push(stepResult);
|
|
21657
|
-
reportProgress(stepIdx, "namedRequest", `run ${targetName} \u2192 ${
|
|
21752
|
+
reportProgress(stepIdx, "namedRequest", `run ${targetName} \u2192 ${requestParsed.method} ${requestParsed.url}`, stepResult);
|
|
21658
21753
|
const relevantCaptures = captures.filter((c) => c.afterRequest === requestIndex);
|
|
21659
21754
|
for (const capture of relevantCaptures) {
|
|
21660
21755
|
const value = getValueByPath(response, capture.path);
|
|
@@ -21732,20 +21827,29 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21732
21827
|
let requestUrl = "";
|
|
21733
21828
|
let requestMethod = "";
|
|
21734
21829
|
try {
|
|
21735
|
-
const
|
|
21736
|
-
requestUrl =
|
|
21737
|
-
requestMethod =
|
|
21738
|
-
const
|
|
21830
|
+
const requestParsed = parserHttpRequest(namedRequest.content, runtimeVariables);
|
|
21831
|
+
requestUrl = requestParsed.url;
|
|
21832
|
+
requestMethod = requestParsed.method;
|
|
21833
|
+
const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
|
|
21834
|
+
const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1e3;
|
|
21835
|
+
const retryOpts = effectiveRetryCount ? {
|
|
21836
|
+
retryCount: effectiveRetryCount,
|
|
21837
|
+
backoffMs: effectiveBackoffMs,
|
|
21838
|
+
onRetry: (attempt, total, error, waitMs) => {
|
|
21839
|
+
reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] var ${varName} = run ${sequenceName} - waiting ${waitMs}ms`, void 0);
|
|
21840
|
+
}
|
|
21841
|
+
} : void 0;
|
|
21842
|
+
const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar, retryOpts) : await sendRequest(requestParsed, retryOpts);
|
|
21739
21843
|
addResponse(response);
|
|
21740
21844
|
const stepResult = {
|
|
21741
21845
|
type: "request",
|
|
21742
21846
|
stepIndex: stepIdx,
|
|
21743
21847
|
response,
|
|
21744
|
-
requestMethod:
|
|
21745
|
-
requestUrl:
|
|
21848
|
+
requestMethod: requestParsed.method,
|
|
21849
|
+
requestUrl: requestParsed.url
|
|
21746
21850
|
};
|
|
21747
21851
|
orderedSteps.push(stepResult);
|
|
21748
|
-
reportProgress(stepIdx, "namedRequest", `var ${varName} = run ${sequenceName} \u2192 ${
|
|
21852
|
+
reportProgress(stepIdx, "namedRequest", `var ${varName} = run ${sequenceName} \u2192 ${requestParsed.method} ${requestParsed.url}`, stepResult);
|
|
21749
21853
|
const capturedResponse = {
|
|
21750
21854
|
status: response.status,
|
|
21751
21855
|
statusText: response.statusText,
|
|
@@ -21951,7 +22055,15 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21951
22055
|
parsed = parserHttpRequest(step.content, runtimeVariables);
|
|
21952
22056
|
requestDescription = `${parsed.method} ${parsed.url}`;
|
|
21953
22057
|
}
|
|
21954
|
-
const
|
|
22058
|
+
const { retryCount, backoffMs } = extractRetryOptions(step.content);
|
|
22059
|
+
const retryOpts = retryCount ?? parsed.retryCount ? {
|
|
22060
|
+
retryCount: retryCount ?? parsed.retryCount ?? 0,
|
|
22061
|
+
backoffMs: backoffMs ?? parsed.backoffMs ?? 1e3,
|
|
22062
|
+
onRetry: (attempt, total, error, waitMs) => {
|
|
22063
|
+
reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] ${requestDescription} - waiting ${waitMs}ms`, void 0);
|
|
22064
|
+
}
|
|
22065
|
+
} : void 0;
|
|
22066
|
+
const response = cookieJar ? await sendRequestWithJar(parsed, cookieJar, retryOpts) : await sendRequest(parsed, retryOpts);
|
|
21955
22067
|
addResponse(response);
|
|
21956
22068
|
const stepResult = {
|
|
21957
22069
|
type: "request",
|
|
@@ -22264,12 +22376,14 @@ function formatResponse(response, options) {
|
|
|
22264
22376
|
const icon = isSuccess ? colors.checkmark : colors.cross;
|
|
22265
22377
|
if (method && url2) {
|
|
22266
22378
|
const displayUrl = redactUrl(url2, redaction);
|
|
22379
|
+
const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1 ? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}` : "";
|
|
22267
22380
|
lines.push(
|
|
22268
|
-
`${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`(${response.status} ${response.statusText})`)} ${colors.duration(formatDuration(response.duration))}`
|
|
22381
|
+
`${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`(${response.status} ${response.statusText})`)} ${colors.duration(formatDuration(response.duration))}${retryInfo}`
|
|
22269
22382
|
);
|
|
22270
22383
|
} else {
|
|
22384
|
+
const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1 ? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}` : "";
|
|
22271
22385
|
lines.push(
|
|
22272
|
-
`${prefix}${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(`(${formatDuration(response.duration)})`)}`
|
|
22386
|
+
`${prefix}${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(`(${formatDuration(response.duration)})`)}${retryInfo}`
|
|
22273
22387
|
);
|
|
22274
22388
|
}
|
|
22275
22389
|
if (verbose || showDetails) {
|
|
@@ -22319,11 +22433,12 @@ function formatResponseCompact(response, options) {
|
|
|
22319
22433
|
const statusColor = getStatusColor(colors, response.status);
|
|
22320
22434
|
const isSuccess = response.status >= 200 && response.status < 300;
|
|
22321
22435
|
const icon = isSuccess ? colors.checkmark : colors.cross;
|
|
22436
|
+
const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1 ? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}` : "";
|
|
22322
22437
|
if (method && url2) {
|
|
22323
22438
|
const displayUrl = redactUrl(url2, redaction);
|
|
22324
|
-
return `${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`${response.status}`)} ${colors.duration(formatDuration(response.duration))}`;
|
|
22439
|
+
return `${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`${response.status}`)} ${colors.duration(formatDuration(response.duration))}${retryInfo}`;
|
|
22325
22440
|
}
|
|
22326
|
-
return `${prefix}${icon} ${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(formatDuration(response.duration))}`;
|
|
22441
|
+
return `${prefix}${icon} ${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(formatDuration(response.duration))}${retryInfo}`;
|
|
22327
22442
|
}
|
|
22328
22443
|
|
|
22329
22444
|
// src/cli/formatters/assertion.ts
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "norn-cli",
|
|
3
3
|
"displayName": "Norn - REST Client",
|
|
4
4
|
"description": "A powerful REST client for making HTTP requests with sequences, variables, scripts, and cookie support",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.3.0",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|