@qaick/http-executor 0.1.0-alpha.1 → 0.1.0-alpha.10
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.
|
@@ -2,7 +2,7 @@ import type { ActionExecutionContext, ActionExecutionResult, ActionExecutor } fr
|
|
|
2
2
|
export interface HttpResponse {
|
|
3
3
|
status: number;
|
|
4
4
|
body: unknown;
|
|
5
|
-
headers?: Record<string, string>;
|
|
5
|
+
headers?: Record<string, string | string[]>;
|
|
6
6
|
}
|
|
7
7
|
export interface HttpTransport {
|
|
8
8
|
request(url: string, init: {
|
|
@@ -68,7 +68,7 @@ export class HttpActionExecutor {
|
|
|
68
68
|
url: appendQuery(url, substituteTemplateValue(context.inputs.query, context.inputs)),
|
|
69
69
|
method: substituteTemplate(String(context.inputs.method ?? 'GET'), context.inputs),
|
|
70
70
|
...(context.inputs.body !== undefined ? { body: substituteTemplateValue(context.inputs.body, context.inputs) } : {}),
|
|
71
|
-
...(isRecord(context.inputs.headers) ? { headers:
|
|
71
|
+
...(isRecord(context.inputs.headers) || isRecord(context.inputs.cookies) ? { headers: renderHeaders(context.inputs.headers, context.inputs.cookies, context.inputs) } : {}),
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
catch (error) {
|
|
@@ -77,11 +77,15 @@ export class HttpActionExecutor {
|
|
|
77
77
|
}
|
|
78
78
|
const timeoutMs = typeof context.step.adapterConfig?.timeoutMs === 'number' ? context.step.adapterConfig.timeoutMs : undefined;
|
|
79
79
|
const response = await this.transport.request(request.url, { ...request, signal: context.signal, ...(timeoutMs !== undefined ? { timeoutMs } : {}) });
|
|
80
|
+
const cookies = responseCookies(response.headers);
|
|
81
|
+
const outputs = { status: response.status, body: response.body, ...(response.headers ? { headers: response.headers } : {}), cookies };
|
|
82
|
+
const responseHeadersName = context.step.adapterConfig?.responseHeadersName;
|
|
83
|
+
if (typeof responseHeadersName === 'string')
|
|
84
|
+
outputs[responseHeadersName] = response.headers ?? {};
|
|
80
85
|
if (response.status < 200 || response.status >= 300) {
|
|
81
|
-
|
|
82
|
-
throw new HttpActionError('HTTP_STATUS_ERROR', `HTTP Action returned status ${response.status}
|
|
86
|
+
debugFailedRequest(request, response.status);
|
|
87
|
+
throw new HttpActionError('HTTP_STATUS_ERROR', `HTTP Action returned status ${response.status}. ${requestDiagnostics(request, response)}`, { status: response.status });
|
|
83
88
|
}
|
|
84
|
-
const outputs = { status: response.status, body: response.body, ...(response.headers ? { headers: response.headers } : {}) };
|
|
85
89
|
const responseFields = isRecord(context.step.adapterConfig?.responseFields) ? context.step.adapterConfig.responseFields : undefined;
|
|
86
90
|
if (responseFields)
|
|
87
91
|
for (const [name, path] of Object.entries(responseFields))
|
|
@@ -90,9 +94,86 @@ export class HttpActionExecutor {
|
|
|
90
94
|
if (typeof context.step.adapterConfig?.responseBodyName === 'string')
|
|
91
95
|
outputs[context.step.adapterConfig.responseBodyName] = response.body;
|
|
92
96
|
const finishedAt = new Date().toISOString();
|
|
93
|
-
return { outputs, evidence: { request: { url: request.url, method: request.method, headers: request.headers }, response: { status: response.status, headers: response.headers, body: response.body } }, timing: { startedAt, finishedAt, durationMs: Date.now() - startedMs } };
|
|
97
|
+
return { outputs, evidence: { request: { url: request.url, method: request.method, headers: request.headers }, response: { status: response.status, headers: response.headers, body: response.body, cookies } }, timing: { startedAt, finishedAt, durationMs: Date.now() - startedMs } };
|
|
94
98
|
}
|
|
95
99
|
}
|
|
100
|
+
function requestDiagnostics(request, response) {
|
|
101
|
+
const parsed = tryParseUrl(request.url);
|
|
102
|
+
const body = request.body === undefined ? undefined : typeof request.body === 'string' ? request.body : JSON.stringify(request.body) ?? '';
|
|
103
|
+
const responseBody = typeof response.body === 'string' ? response.body : JSON.stringify(response.body) ?? '';
|
|
104
|
+
return `Request diagnostics: method=${request.method} path=${parsed?.pathname ?? '(invalid)'} bodyBytes=${byteLength(body)} requestHeaderNames=${headerNames(request.headers).join(',') || '(none)'} requestCookieNames=${cookieNames(request.headers).join(',') || '(none)'} requestCookieValueLengths=${cookieValueLengths(request.headers) || '(none)'} requestAuthHeaderLengths=${authHeaderLengths(request.headers) || '(none)'} responseHeaderNames=${headerNames(response.headers).join(',') || '(none)'} responseBodyBytes=${byteLength(responseBody)}.`;
|
|
105
|
+
}
|
|
106
|
+
function debugFailedRequest(request, status) {
|
|
107
|
+
if (typeof process === 'undefined' || process.env.QAICK_HTTP_DEBUG_FULL_REQUEST !== '1')
|
|
108
|
+
return;
|
|
109
|
+
console.error('[QAICK_HTTP_DEBUG_FULL_REQUEST]', { status, request });
|
|
110
|
+
}
|
|
111
|
+
function tryParseUrl(value) {
|
|
112
|
+
try {
|
|
113
|
+
return new URL(value);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function headerNames(headers) {
|
|
120
|
+
return Object.keys(headers ?? {}).map(name => name.toLowerCase()).sort();
|
|
121
|
+
}
|
|
122
|
+
function byteLength(value) {
|
|
123
|
+
return value === undefined ? 0 : new TextEncoder().encode(value).byteLength;
|
|
124
|
+
}
|
|
125
|
+
function cookieNames(headers) {
|
|
126
|
+
const cookie = Object.entries(headers ?? {}).find(([name]) => name.toLowerCase() === 'cookie')?.[1];
|
|
127
|
+
return cookie ? cookie.split(';').map(item => item.trim().split('=', 1)[0]).filter(Boolean) : [];
|
|
128
|
+
}
|
|
129
|
+
function cookieValueLengths(headers) {
|
|
130
|
+
const cookie = Object.entries(headers ?? {}).find(([name]) => name.toLowerCase() === 'cookie')?.[1];
|
|
131
|
+
return cookie ? cookie.split(';').map(item => item.trim()).filter(Boolean).map(item => {
|
|
132
|
+
const separator = item.indexOf('=');
|
|
133
|
+
return separator > 0 ? `${item.slice(0, separator)}:${item.length - separator - 1}` : `${item}:0`;
|
|
134
|
+
}).join(',') : '';
|
|
135
|
+
}
|
|
136
|
+
function authHeaderLengths(headers) {
|
|
137
|
+
return Object.entries(headers ?? {}).filter(([name]) => ['x-auth-token', 'x-xsrf-token'].includes(name.toLowerCase())).map(([name, value]) => `${name.toLowerCase()}:${value.length}`).join(',');
|
|
138
|
+
}
|
|
139
|
+
function responseCookies(headers) {
|
|
140
|
+
const raw = headers && Object.entries(headers).find(([name]) => name.toLowerCase() === 'set-cookie')?.[1];
|
|
141
|
+
const values = Array.isArray(raw) ? raw : raw ? raw.split(/\r?\n|,(?=\s*[^;,=\s]+=[^;,]*)/).map(item => item.trim()).filter(Boolean) : [];
|
|
142
|
+
return Object.fromEntries(values.flatMap(header => {
|
|
143
|
+
const [pair, ...parts] = header.split(';').map(item => item.trim());
|
|
144
|
+
const separator = pair?.indexOf('=') ?? -1;
|
|
145
|
+
if (!pair || separator <= 0)
|
|
146
|
+
return [];
|
|
147
|
+
const name = pair.slice(0, separator).trim();
|
|
148
|
+
const value = pair.slice(separator + 1).trim();
|
|
149
|
+
const attributes = {};
|
|
150
|
+
for (const part of parts) {
|
|
151
|
+
const equals = part.indexOf('=');
|
|
152
|
+
if (equals > 0)
|
|
153
|
+
attributes[part.slice(0, equals).trim().toLowerCase()] = part.slice(equals + 1).trim();
|
|
154
|
+
else if (part)
|
|
155
|
+
attributes[part.toLowerCase()] = true;
|
|
156
|
+
}
|
|
157
|
+
return [[name, { value, attributes }]];
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
function renderHeaders(headersInput, cookiesInput, templateInputs) {
|
|
161
|
+
const headers = isRecord(headersInput)
|
|
162
|
+
? Object.fromEntries(Object.entries(substituteTemplateValue(headersInput, templateInputs)).map(([name, value]) => [name, String(value)]))
|
|
163
|
+
: {};
|
|
164
|
+
if (!isRecord(cookiesInput) || Object.keys(headers).some(name => name.toLowerCase() === 'cookie'))
|
|
165
|
+
return headers;
|
|
166
|
+
const cookies = Object.entries(cookiesInput).flatMap(([name, value]) => {
|
|
167
|
+
if (isRecord(value) && typeof value.value === 'string')
|
|
168
|
+
return [`${name}=${value.value}`];
|
|
169
|
+
if (typeof value === 'string')
|
|
170
|
+
return [`${name}=${value}`];
|
|
171
|
+
return [];
|
|
172
|
+
});
|
|
173
|
+
if (cookies.length)
|
|
174
|
+
headers.Cookie = cookies.join('; ');
|
|
175
|
+
return headers;
|
|
176
|
+
}
|
|
96
177
|
function appendQuery(url, query) {
|
|
97
178
|
if (query === undefined || query === null)
|
|
98
179
|
return url;
|
|
@@ -8,7 +8,7 @@ export class NodeHttpTransport {
|
|
|
8
8
|
if (init.signal.aborted)
|
|
9
9
|
return Promise.reject(new HttpActionError('HTTP_ABORTED', 'HTTP Action was cancelled before a response was received.'));
|
|
10
10
|
const parsed = new URL(url);
|
|
11
|
-
const headers = { ...init.headers };
|
|
11
|
+
const headers = { accept: '*/*', 'user-agent': 'QAICK-Desktop', ...init.headers };
|
|
12
12
|
let body;
|
|
13
13
|
if (init.body !== undefined && init.body !== null) {
|
|
14
14
|
body = typeof init.body === 'string' ? init.body : JSON.stringify(init.body);
|
|
@@ -76,7 +76,7 @@ export class NodeHttpTransport {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
function normalizeHeaders(headers) {
|
|
79
|
-
return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, Array.isArray(value) ? value
|
|
79
|
+
return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, Array.isArray(value) ? value : value ?? '']));
|
|
80
80
|
}
|
|
81
81
|
function headerValue(headers, name) {
|
|
82
82
|
const value = headers[name];
|
package/package.json
CHANGED
|
@@ -1,19 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qaick/http-executor",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.10",
|
|
4
4
|
"description": "Reusable QAick HTTP Action execution with browser and native Node transports.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"qaick",
|
|
7
|
+
"http",
|
|
8
|
+
"executor",
|
|
9
|
+
"automation"
|
|
10
|
+
],
|
|
6
11
|
"type": "module",
|
|
7
|
-
"files": [
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"tag": "alpha"
|
|
22
|
+
},
|
|
10
23
|
"main": "./dist/index.js",
|
|
11
24
|
"types": "./dist/index.d.ts",
|
|
12
25
|
"exports": {
|
|
13
|
-
".": {
|
|
14
|
-
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./node": {
|
|
31
|
+
"types": "./dist/node-http-transport.d.ts",
|
|
32
|
+
"import": "./dist/node-http-transport.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"prepublishOnly": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@qaick/executor": ">=0.1.0-alpha.1 <0.2.0"
|
|
15
43
|
},
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@qaick/executor": "0.1.0-alpha.2",
|
|
46
|
+
"@types/node": "^24.13.3",
|
|
47
|
+
"typescript": "~6.0.2",
|
|
48
|
+
"vitest": "^5.0.1"
|
|
49
|
+
}
|
|
19
50
|
}
|