@qaick/http-executor 0.1.0-alpha.2 → 0.1.0-alpha.4
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.
|
@@ -13,6 +13,8 @@ export interface HttpTransport {
|
|
|
13
13
|
timeoutMs?: number;
|
|
14
14
|
}): Promise<HttpResponse>;
|
|
15
15
|
}
|
|
16
|
+
/** Internal Runtime-to-HTTP execution key. It is not part of persisted Action data. */
|
|
17
|
+
export declare const HTTP_EXECUTION_ID_KEY = "__qaick_execution_id";
|
|
16
18
|
export interface HttpCookie {
|
|
17
19
|
name: string;
|
|
18
20
|
value: string;
|
|
@@ -58,7 +60,9 @@ export declare class FetchHttpTransport implements HttpTransport {
|
|
|
58
60
|
}
|
|
59
61
|
export declare class HttpActionExecutor implements ActionExecutor {
|
|
60
62
|
private readonly transport;
|
|
63
|
+
private readonly sessions;
|
|
61
64
|
constructor(transport: HttpTransport);
|
|
62
65
|
supports(action: string): boolean;
|
|
63
66
|
execute(context: ActionExecutionContext): Promise<ActionExecutionResult>;
|
|
67
|
+
private sessionFor;
|
|
64
68
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Internal Runtime-to-HTTP execution key. It is not part of persisted Action data. */
|
|
2
|
+
export const HTTP_EXECUTION_ID_KEY = '__qaick_execution_id';
|
|
1
3
|
/** Cookie state shared by HTTP Actions during one execution. */
|
|
2
4
|
export class HttpCookieJar {
|
|
3
5
|
cookies = new Map();
|
|
@@ -42,16 +44,31 @@ export class HttpExecutionSession {
|
|
|
42
44
|
this.cookies = cookies;
|
|
43
45
|
}
|
|
44
46
|
async request(url, init) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
let currentUrl = url;
|
|
48
|
+
let currentInit = { ...init };
|
|
49
|
+
for (let redirect = 0; redirect <= 10; redirect += 1) {
|
|
50
|
+
const headers = { ...(currentInit.headers ?? {}) };
|
|
51
|
+
const cookieHeader = headerName(headers, 'cookie');
|
|
52
|
+
const managed = this.cookies.header(currentUrl);
|
|
53
|
+
const merged = mergeCookieHeaders(cookieHeader ? headers[cookieHeader] : undefined, managed);
|
|
54
|
+
if (merged)
|
|
55
|
+
headers[cookieHeader ?? 'cookie'] = merged;
|
|
56
|
+
const response = await this.transport.request(currentUrl, { ...currentInit, headers });
|
|
57
|
+
const setCookie = response.headers && headerValue(response.headers, 'set-cookie');
|
|
58
|
+
this.cookies.store(currentUrl, setCookie);
|
|
59
|
+
if (!isRedirect(response.status) || !response.headers)
|
|
60
|
+
return response;
|
|
61
|
+
const location = headerValue(response.headers, 'location');
|
|
62
|
+
if (!location)
|
|
63
|
+
return response;
|
|
64
|
+
if (redirect === 10)
|
|
65
|
+
return response;
|
|
66
|
+
const nextUrl = new URL(location, currentUrl).toString();
|
|
67
|
+
const nextMethod = response.status === 303 || ((response.status === 301 || response.status === 302) && currentInit.method.toUpperCase() === 'POST') ? 'GET' : currentInit.method;
|
|
68
|
+
currentUrl = nextUrl;
|
|
69
|
+
currentInit = nextMethod === 'GET' ? { ...currentInit, method: nextMethod, body: undefined } : { ...currentInit, method: nextMethod };
|
|
70
|
+
}
|
|
71
|
+
throw new Error('HTTP redirect limit exceeded.');
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
74
|
export class HttpActionError extends Error {
|
|
@@ -71,6 +88,7 @@ function headerValue(headers, expected) {
|
|
|
71
88
|
const name = Object.keys(headers).find(key => key.toLowerCase() === expected);
|
|
72
89
|
return name ? headers[name] : undefined;
|
|
73
90
|
}
|
|
91
|
+
function isRedirect(status) { return status === 301 || status === 302 || status === 303 || status === 307 || status === 308; }
|
|
74
92
|
function mergeCookieHeaders(explicit, managed) {
|
|
75
93
|
const cookies = new Map();
|
|
76
94
|
for (const value of [managed, explicit]) {
|
|
@@ -187,6 +205,7 @@ export class FetchHttpTransport {
|
|
|
187
205
|
}
|
|
188
206
|
export class HttpActionExecutor {
|
|
189
207
|
transport;
|
|
208
|
+
sessions = new Map();
|
|
190
209
|
constructor(transport) {
|
|
191
210
|
this.transport = transport;
|
|
192
211
|
}
|
|
@@ -209,7 +228,9 @@ export class HttpActionExecutor {
|
|
|
209
228
|
throw new HttpActionError('HTTP_REQUEST_CONSTRUCTION_ERROR', `HTTP Action request could not be constructed. ${detail}`);
|
|
210
229
|
}
|
|
211
230
|
const timeoutMs = typeof context.step.adapterConfig?.timeoutMs === 'number' ? context.step.adapterConfig.timeoutMs : undefined;
|
|
212
|
-
const
|
|
231
|
+
const executionId = typeof context.runtimeValues[HTTP_EXECUTION_ID_KEY] === 'string' ? context.runtimeValues[HTTP_EXECUTION_ID_KEY] : undefined;
|
|
232
|
+
const transport = executionId ? this.sessionFor(executionId) : this.transport;
|
|
233
|
+
const response = await transport.request(request.url, { ...request, signal: context.signal, ...(timeoutMs !== undefined ? { timeoutMs } : {}) });
|
|
213
234
|
if (response.status < 200 || response.status >= 300) {
|
|
214
235
|
const serializedBody = typeof response.body === 'string' ? response.body : JSON.stringify(response.body) ?? '';
|
|
215
236
|
throw new HttpActionError('HTTP_STATUS_ERROR', `HTTP Action returned status ${response.status}.${serializedBody ? ` Response: ${serializedBody.slice(0, 500)}` : ''}`, { status: response.status });
|
|
@@ -225,6 +246,14 @@ export class HttpActionExecutor {
|
|
|
225
246
|
const finishedAt = new Date().toISOString();
|
|
226
247
|
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 } };
|
|
227
248
|
}
|
|
249
|
+
sessionFor(executionId) {
|
|
250
|
+
let session = this.sessions.get(executionId);
|
|
251
|
+
if (!session) {
|
|
252
|
+
session = new HttpExecutionSession(this.transport);
|
|
253
|
+
this.sessions.set(executionId, session);
|
|
254
|
+
}
|
|
255
|
+
return session;
|
|
256
|
+
}
|
|
228
257
|
}
|
|
229
258
|
function appendQuery(url, query) {
|
|
230
259
|
if (query === undefined || query === null)
|