From c53e5ab449996261aa0711e63f032448d8d68f43 Mon Sep 17 00:00:00 2001 From: Amr Elmohamady Date: Thu, 3 Sep 2026 21:40:06 +0300 Subject: [PATCH 1/2] ci(pieces): fail PR when a piece's heap usage regresses (#15252) Co-authored-by: Claude Opus 4.7 --- .../validate-publishable-packages.yml | 29 +++ tools/scripts/pieces/check-pieces-heap.ts | 187 ++++++++++++++++++ tools/scripts/pieces/heap-check-child.mjs | 40 ++++ 3 files changed, 256 insertions(+) create mode 100644 tools/scripts/pieces/check-pieces-heap.ts create mode 100644 tools/scripts/pieces/heap-check-child.mjs diff --git a/.github/workflows/validate-publishable-packages.yml b/.github/workflows/validate-publishable-packages.yml index 3021b6e5dc1d..8453de1358e3 100644 --- a/.github/workflows/validate-publishable-packages.yml +++ b/.github/workflows/validate-publishable-packages.yml @@ -39,3 +39,32 @@ jobs: - name: validate publishable packages run: npx ts-node -r tsconfig-paths/register -P packages/server/engine/tsconfig.lib.json tools/scripts/validate-publishable-packages.ts + + - name: find changed pieces + id: changed + run: | + OUTPUT=$(npx ts-node -r tsconfig-paths/register -P packages/server/engine/tsconfig.lib.json tools/scripts/pieces/find-changed-pieces.ts) + TURBO_FILTER=$(echo "$OUTPUT" | grep '^TURBO_FILTER:' | sed 's/^TURBO_FILTER://') + CHANGED_DIRS=$(echo "$OUTPUT" | sed -n '/^CHANGED_DIRS:$/,/^TURBO_FILTER:/{ /^CHANGED_DIRS:$/d; /^TURBO_FILTER:/d; p; }') + if [ -z "$TURBO_FILTER" ]; then + echo "No changed pieces found" + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "turbo_filter=$TURBO_FILTER" >> "$GITHUB_OUTPUT" + { + echo "changed_dirs<> "$GITHUB_OUTPUT" + fi + + - name: build changed pieces + if: steps.changed.outputs.has_changes == 'true' + run: npx turbo run build ${{ steps.changed.outputs.turbo_filter }} + + - name: check piece heap usage + if: steps.changed.outputs.has_changes == 'true' && !contains(github.event.pull_request.labels.*.name, 'skip-heap-check') + env: + CHANGED_PIECES: ${{ steps.changed.outputs.changed_dirs }} + run: npx ts-node -r tsconfig-paths/register -P packages/server/engine/tsconfig.lib.json tools/scripts/pieces/check-pieces-heap.ts diff --git a/tools/scripts/pieces/check-pieces-heap.ts b/tools/scripts/pieces/check-pieces-heap.ts new file mode 100644 index 000000000000..57815b516d5b --- /dev/null +++ b/tools/scripts/pieces/check-pieces-heap.ts @@ -0,0 +1,187 @@ +import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { spawnSync } from 'node:child_process' +import { chunk } from '@activepieces/core-utils' +import { findAllPiecesDirectoryInSource } from '../utils/piece-script-utils' +import { preparePieceDistForPublish } from '../../../packages/cli/src/lib/utils/prepare-piece-utils' +import { readPackageJson } from '../utils/files' + +async function bundleIfNeeded(piecePath: string): Promise { + const bundleEntry = join(piecePath, 'dist', 'index.bundle.js') + if (existsSync(bundleEntry)) { + return + } + await preparePieceDistForPublish(piecePath) +} + +function runNode(args: string[], cwd: string, timeoutMs: number): { status: number | null; stdout: string; stderr: string } { + const r = spawnSync('node', args, { cwd, timeout: timeoutMs, encoding: 'utf-8' }) + return { status: r.status, stdout: r.stdout, stderr: r.stderr } +} + +function installIntoScratch(spec: string, scratch: string, timeoutMs: number): { ok: boolean; error?: string } { + writeFileSync(join(scratch, 'package.json'), JSON.stringify({ name: 'ap-heap-probe', version: '0.0.0', private: true })) + const install = spawnSync('npm', [ + 'install', spec, + '--no-audit', '--no-fund', '--no-save', + '--prefer-offline', '--production', '--force', + '--loglevel=error', + ], { cwd: scratch, timeout: timeoutMs, encoding: 'utf-8' }) + if (install.status !== 0) { + return { ok: false, error: 'install-failed: ' + (install.stderr || install.stdout || '').slice(0, 240) } + } + return { ok: true } +} + +function measureFromNodeModules(scratch: string, pkgName: string): { ok: boolean; heapMB?: number; error?: string } { + const distFolder = join(scratch, 'node_modules', pkgName) + if (!existsSync(distFolder)) { + return { ok: false, error: `piece dir missing from scratch node_modules: ${distFolder}` } + } + const measure = runNode(['--expose-gc', resolve(__dirname, 'heap-check-child.mjs'), distFolder], scratch, 90_000) + if (measure.status !== 0) { + return { ok: false, error: 'child-crashed: ' + (measure.stderr || '').slice(0, 240) } + } + const line = measure.stdout.trim().split('\n').pop() ?? '' + const parsed = JSON.parse(line) as { heapDeltaBytes?: number; error?: string } + if (parsed.error) return { ok: false, error: parsed.error } + return { ok: true, heapMB: parsed.heapDeltaBytes! / 1e6 } +} + +function measurePreviousPublished(pkgName: string): { ok: boolean; heapMB?: number; version?: string; error?: string; missing?: boolean } { + const view = spawnSync('npm', ['view', pkgName, 'version', '--json'], { timeout: 60_000, encoding: 'utf-8' }) + if (view.status !== 0) { + const stderr = (view.stderr || '').toString() + if (stderr.includes('E404')) { + return { ok: true, missing: true } + } + return { ok: false, error: 'npm-view-failed: ' + stderr.slice(0, 240) } + } + const version = JSON.parse(view.stdout.trim()) as string + + const scratch = mkdtempSync(join(tmpdir(), 'ap-heap-prev-')) + try { + const install = installIntoScratch(`${pkgName}@${version}`, scratch, 5 * 60_000) + if (!install.ok) return { ok: false, error: install.error, version } + const m = measureFromNodeModules(scratch, pkgName) + return { ...m, version } + } finally { + rmSync(scratch, { recursive: true, force: true }) + } +} + +async function measureCurrent(piecePath: string, pkgName: string): Promise<{ ok: boolean; heapMB?: number; error?: string }> { + await bundleIfNeeded(piecePath) + const distPath = resolve(piecePath, 'dist') + const scratch = mkdtempSync(join(tmpdir(), 'ap-heap-cur-')) + try { + const install = installIntoScratch(`file:${distPath}`, scratch, 5 * 60_000) + if (!install.ok) return { ok: false, error: install.error } + return measureFromNodeModules(scratch, pkgName) + } finally { + rmSync(scratch, { recursive: true, force: true }) + } +} + +async function checkOne(piecePath: string): Promise { + const pkg = await readPackageJson(piecePath) + const [current, prev] = await Promise.all([ + measureCurrent(piecePath, pkg.name), + Promise.resolve(measurePreviousPublished(pkg.name)), + ]) + if (!current.ok) { + return { name: pkg.name, ok: false, error: `current: ${current.error}` } + } + return { + name: pkg.name, + ok: true, + heapMB: current.heapMB!, + previousHeapMB: prev.ok && !prev.missing ? prev.heapMB : undefined, + previousVersion: prev.version, + previousMissing: prev.missing === true, + previousError: !prev.ok ? prev.error : undefined, + } +} + +function verdict(r: PieceHeapResult): { failed: boolean; reasons: string[] } { + const reasons: string[] = [] + if (!r.ok) { + reasons.push(`could not measure current build: ${r.error ?? 'unknown error'}`) + return { failed: true, reasons } + } + if (r.previousError !== undefined) { + reasons.push(`could not measure previously-published version to verify delta: ${r.previousError}`) + return { failed: true, reasons } + } + if (r.previousMissing === true) { + if (r.heapMB! > ABSOLUTE_LIMIT_MB) { + reasons.push(`heap ${r.heapMB!.toFixed(2)} MB > absolute limit ${ABSOLUTE_LIMIT_MB} MB (new piece)`) + } + return { failed: reasons.length > 0, reasons } + } + const delta = r.heapMB! - r.previousHeapMB! + if (delta > DELTA_LIMIT_MB) { + reasons.push(`heap grew +${delta.toFixed(2)} MB vs ${r.previousVersion} (${r.previousHeapMB!.toFixed(2)} → ${r.heapMB!.toFixed(2)} MB); allowed +${DELTA_LIMIT_MB} MB`) + } + return { failed: reasons.length > 0, reasons } +} + +async function main(): Promise { + const changed = process.env['CHANGED_PIECES'] + const piecePaths = changed + ? changed.split('\n').filter(Boolean) + : await findAllPiecesDirectoryInSource() + + console.info(`[checkPiecesHeap] checking ${piecePaths.length} piece(s)${changed ? ' (scoped to changed)' : ' (all)'} new pieces: heap <= ${ABSOLUTE_LIMIT_MB} MB existing pieces: delta <= +${DELTA_LIMIT_MB} MB vs latest published`) + + const results: PieceHeapResult[] = [] + for (const batch of chunk(piecePaths, 3)) { + results.push(...await Promise.all(batch.map(checkOne))) + } + + let failed = 0 + for (const r of results) { + const v = verdict(r) + if (v.failed) failed++ + const tag = v.failed ? 'FAIL' : 'ok ' + const heap = r.ok ? `${r.heapMB!.toFixed(2)} MB` : '—' + const prev = r.previousHeapMB !== undefined + ? ` (prev ${r.previousVersion}: ${r.previousHeapMB.toFixed(2)} MB)` + : r.previousMissing + ? ' (new piece, never published)' + : '' + console.info(` [${tag}] ${r.name.padEnd(46)} ${heap.padStart(10)}${prev}`) + for (const reason of v.reasons) { + console.info(` └─ ${reason}`) + } + } + + console.info('') + console.info(`[checkPiecesHeap] checked=${results.length} failed=${failed}`) + + if (failed > 0) { + console.info(`\nTo bypass this check on a PR, apply the "${SKIP_LABEL}" label. Only use it when you understand the cost.`) + process.exit(1) + } +} + +const ABSOLUTE_LIMIT_MB = Number(process.env['PIECE_HEAP_ABSOLUTE_MB'] ?? '10') +const DELTA_LIMIT_MB = Number(process.env['PIECE_HEAP_DELTA_MB'] ?? '2') +const SKIP_LABEL = 'skip-heap-check' + +type PieceHeapResult = { + name: string + ok: boolean + heapMB?: number + previousHeapMB?: number + previousVersion?: string + previousMissing?: boolean + previousError?: string + error?: string +} + +main().catch(err => { + console.error(err) + process.exit(2) +}) diff --git a/tools/scripts/pieces/heap-check-child.mjs b/tools/scripts/pieces/heap-check-child.mjs new file mode 100644 index 000000000000..956c5d3e5422 --- /dev/null +++ b/tools/scripts/pieces/heap-check-child.mjs @@ -0,0 +1,40 @@ +import { createRequire } from 'node:module' +import { resolve } from 'node:path' + +const distFolder = process.argv[2] +if (!distFolder) { + console.error('[heap-check-child] missing dist folder argv') + process.exit(2) +} + +if (typeof global.gc !== 'function') { + console.error('[heap-check-child] run with --expose-gc') + process.exit(2) +} + +const pkg = createRequire(import.meta.url)(resolve(distFolder, 'package.json')) +const entryRel = pkg.main ?? 'index.js' +const entry = resolve(distFolder, entryRel) + +const require = createRequire(import.meta.url) + +global.gc() +const before = process.memoryUsage() +const t0 = process.hrtime.bigint() +try { + require(entry) +} catch (e) { + console.log(JSON.stringify({ error: String(e).split('\n')[0].slice(0, 240) })) + process.exit(0) +} +const t1 = process.hrtime.bigint() +global.gc() +const after = process.memoryUsage() + +console.log(JSON.stringify({ + heapDeltaBytes: after.heapUsed - before.heapUsed, + heapAfterBytes: after.heapUsed, + rssDeltaBytes: after.rss - before.rss, + externalDeltaBytes: after.external - before.external, + requireMs: Number(t1 - t0) / 1e6, +})) From 219c644f21e1d887d8a27e93f1d99da87e725bb4 Mon Sep 17 00:00:00 2001 From: Talal Jaber Date: Thu, 3 Sep 2026 21:52:29 +0300 Subject: [PATCH 2/2] feat(http): improve the Send HTTP request step form (#15188) Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: ibrahim-abuznaid --- bun.lock | 2 +- packages/pieces/core/http/package.json | 2 +- .../core/http/src/i18n/translation.json | 23 +- packages/pieces/core/http/src/index.ts | 4 +- .../lib/actions/send-http-request-action.ts | 220 +++++++++--------- .../pieces/core/http/src/lib/common/props.ts | 2 + 6 files changed, 129 insertions(+), 124 deletions(-) diff --git a/bun.lock b/bun.lock index aead8a638454..6a1614f5cf0f 100644 --- a/bun.lock +++ b/bun.lock @@ -10398,7 +10398,7 @@ }, "packages/pieces/core/http": { "name": "@activepieces/piece-http", - "version": "0.11.19", + "version": "0.11.20", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", diff --git a/packages/pieces/core/http/package.json b/packages/pieces/core/http/package.json index f7d1f5498512..2757f174263e 100644 --- a/packages/pieces/core/http/package.json +++ b/packages/pieces/core/http/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-http", - "version": "0.11.19", + "version": "0.11.20", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { diff --git a/packages/pieces/core/http/src/i18n/translation.json b/packages/pieces/core/http/src/i18n/translation.json index d87105049584..5a44527c474c 100644 --- a/packages/pieces/core/http/src/i18n/translation.json +++ b/packages/pieces/core/http/src/i18n/translation.json @@ -1,6 +1,9 @@ { - "Sends HTTP requests and return responses": "Sends HTTP requests and return responses", + "Send HTTP requests to any URL and use the response in your flow": "Send HTTP requests to any URL and use the response in your flow", "Send HTTP request": "Send HTTP request", + "Parse URL": "Parse URL", + "Call any URL with a chosen method, optional authentication and body.": "Call any URL with a chosen method, optional authentication and body.", + "Extract the domain, path, and query parameters from a URL.": "Extract the domain, path, and query parameters from a URL.", "Method": "Method", "URL": "URL", "Headers": "Headers", @@ -12,11 +15,21 @@ "Response is Binary": "Response is Binary", "Use Proxy": "Use Proxy", "Proxy Settings": "Proxy Settings", - "Timeout(in seconds)": "Timeout(in seconds)", + "Timeout": "Timeout", "Follow redirects": "Follow redirects", "On Failure": "On Failure", - "Enable for files like PDFs, images, etc. A base64 body will be returned.": "Enable for files like PDFs, images, etc. A base64 body will be returned.", - "Use a proxy for this request": "Use a proxy for this request", + "Return Values as Arrays": "Return Values as Arrays", + "GET reads data, POST creates it.": "GET reads data, POST creates it.", + "The full URL to call, including https:// or http://": "The full URL to call, including https:// or http://", + "Sent with the request as key-value pairs.": "Sent with the request as key-value pairs.", + "Appended to the URL as a query string.": "Appended to the URL as a query string.", + "How to encode the request body. Leave as None for GET requests.": "How to encode the request body. Leave as None for GET requests.", + "Return the response as base64, for files like PDFs.": "Return the response as base64, for files like PDFs.", + "Route this request through an HTTP proxy.": "Route this request through an HTTP proxy.", + "Seconds to wait for a response. Empty: up to the flow limit (10 min).": "Seconds to wait for a response. Empty: up to the flow limit (10 min).", + "Follow 3xx redirects instead of returning the response.": "Follow 3xx redirects instead of returning the response.", + "The full URL you want to parse (e.g., https://example.com/page?utm_source=twitter)": "The full URL you want to parse (e.g., https://example.com/page?utm_source=twitter)", + "If checked (recommended), all query parameters are returned as JSON arrays to safely group duplicate keys (e.g., {\"tags\": [\"shoes\", \"hats\"]}). If unchecked, standard web behavior applies, returning only the first value as text.": "If checked (recommended), all query parameters are returned as JSON arrays to safely group duplicate keys (e.g., {\"tags\": [\"shoes\", \"hats\"]}). If unchecked, standard web behavior applies, returning only the first value as text.", "GET": "GET", "POST": "POST", "PATCH": "PATCH", @@ -26,8 +39,8 @@ "None": "None", "Basic Auth": "Basic Auth", "Bearer Token": "Bearer Token", - "Form Data": "Form Data", "JSON": "JSON", + "Form Data": "Form Data", "Raw": "Raw", "Retry on all errors (4xx, 5xx)": "Retry on all errors (4xx, 5xx)", "Retry on internal errors (5xx)": "Retry on internal errors (5xx)", diff --git a/packages/pieces/core/http/src/index.ts b/packages/pieces/core/http/src/index.ts index 8abfac6a9b89..e65c696456e2 100644 --- a/packages/pieces/core/http/src/index.ts +++ b/packages/pieces/core/http/src/index.ts @@ -4,11 +4,11 @@ import { parseUrl } from './lib/actions/parse-url'; export const http = createPiece({ displayName: 'HTTP', - description: 'Sends HTTP requests and return responses', + description: 'Send HTTP requests to any URL and use the response in your flow', logoUrl: 'https://cdn.activepieces.com/pieces/new-core/http.svg', categories: [PieceCategory.CORE], auth: PieceAuth.None(), - minimumSupportedRelease: '0.20.3', + minimumSupportedRelease: '0.88.2', actions: [httpSendRequestAction, parseUrl], authors: [ 'bibhuty-did-this', diff --git a/packages/pieces/core/http/src/lib/actions/send-http-request-action.ts b/packages/pieces/core/http/src/lib/actions/send-http-request-action.ts index 98147a825726..6a2eaec44b9b 100644 --- a/packages/pieces/core/http/src/lib/actions/send-http-request-action.ts +++ b/packages/pieces/core/http/src/lib/actions/send-http-request-action.ts @@ -30,25 +30,29 @@ export const httpSendRequestAction = createAction({ name: 'send_request', classification: 'WRITE', displayName: 'Send HTTP request', - description: 'Send HTTP request', + description: 'Call any URL with a chosen method, optional authentication and body.', aiMetadata: { description: 'Sends an HTTP request to any URL with a chosen method, optional Basic or Bearer auth, an optional JSON, raw or multipart body, and can retry or continue the flow on 4xx/5xx. Use it as the generic escape hatch for an API with no dedicated piece — prefer that app\'s own piece when one exists, and Parse URL to pull a URL apart without calling it. Requires an absolute URL and a method; not idempotent, since a call\'s effect follows the method and POST/PATCH-style calls mutate remote data.', idempotent: false }, props: { method: httpMethodDropdown, url: Property.ShortText({ displayName: 'URL', + description: 'The full URL to call, including https:// or http://', required: true, + placeholder: 'https://api.example.com/v1/users', }), headers: Property.Object({ displayName: 'Headers', - required: true, + description: 'Sent with the request as key-value pairs.', + required: false, }), queryParams: Property.Object({ displayName: 'Query params', - required: true, + description: 'Appended to the URL as a query string.', + required: false, }), authType: Property.StaticDropdown({ displayName: 'Authentication', - required: true, + required: false, defaultValue: AuthType.NONE, options: { disabled: false, @@ -64,68 +68,45 @@ export const httpSendRequestAction = createAction({ required: false, auth: PieceAuth.None(), refreshers: ['authType'], - props: async ({ authType }) => { - if (!authType) { - return {}; + props: async ({ authType }): Promise => { + if (authType === AuthType.BASIC) { + return { + username: Property.ShortText({ + displayName: 'Username', + description: 'The username for authentication.', + required: true, + }), + password: Property.ShortText({ + displayName: 'Password', + description: 'Stored in the flow and visible in exports. Prefer HTTP (OAuth2).', + required: true, + }), + }; } - const authTypeEnum = authType.toString() as AuthType; - let fields: DynamicPropsValue = {}; - switch (authTypeEnum) { - case AuthType.NONE: - fields = {}; - break; - case AuthType.BASIC: - fields = { - username: Property.ShortText({ - displayName: 'Username', - description: 'The username to use for authentication.', - required: true, - }), - password: Property.ShortText({ - displayName: 'Password', - description: 'The password to use for authentication.', - required: true, - }), - }; - break; - case AuthType.BEARER_TOKEN: - fields = { - token: Property.ShortText({ - displayName: 'Token', - description: 'The Bearer token to use for authentication.', - required: true, - }), - }; - break; - default: - throw new Error('Invalid authentication type'); + if (authType === AuthType.BEARER_TOKEN) { + return { + token: Property.ShortText({ + displayName: 'Token', + description: 'Stored in the flow and visible in exports. Prefer HTTP (OAuth2).', + required: true, + }), + }; } - return fields; + return {}; }, }), body_type: Property.StaticDropdown({ displayName: 'Body Type', + description: 'How to encode the request body. Leave as None for GET requests.', required: false, defaultValue: 'none', options: { disabled: false, options: [ - { - label: 'None', - value: 'none', - }, - { - label: 'Form Data', - value: 'form_data', - }, - { - label: 'JSON', - value: 'json', - }, - { - label: 'Raw', - value: 'raw', - }, + { label: 'None', value: 'none' }, + { label: 'JSON', value: 'json' }, + { label: 'Form Data', value: 'form_data' }, + { label: 'Raw', value: 'raw' }, ], }, }), @@ -134,36 +115,32 @@ export const httpSendRequestAction = createAction({ refreshers: ['body_type'], required: false, auth: PieceAuth.None(), - props: async ({ body_type }) => { - if (!body_type) return {}; - - const bodyTypeInput = body_type as unknown as string; - - const fields: DynamicPropsValue = {}; - - switch (bodyTypeInput) { - case 'none': - break; - case 'json': - fields['data'] = Property.Json({ + props: async ({ body_type }): Promise => { + if (body_type === 'json') { + return { + data: Property.Json({ displayName: 'JSON Body', required: true, - }); - break; - case 'raw': - fields['data'] = Property.LongText({ + }), + }; + } + if (body_type === 'raw') { + return { + data: Property.LongText({ displayName: 'Raw Body', required: true, - }); - break; - case 'form_data': - fields['data'] = Property.Array({ + }), + }; + } + if (body_type === 'form_data') { + return { + data: Property.Array({ displayName: 'Form Data', required: true, properties: { fieldName: Property.ShortText({ displayName: 'Field Name', - required: true + required: true, }), fieldType: Property.StaticDropdown({ displayName: 'Field Type', @@ -172,79 +149,83 @@ export const httpSendRequestAction = createAction({ disabled: false, options: [ { label: 'Text', value: 'text' }, - { label: 'File', value: 'file' } - ] - } + { label: 'File', value: 'file' }, + ], + }, }), textFieldValue: Property.LongText({ displayName: 'Text Field Value', - required: false + required: false, }), fileFieldValue: Property.File({ displayName: 'File Field Value', - required: false - }) - } - }); - break; + required: false, + }), + }, + }), + }; } - return fields; + return {}; }, }), response_is_binary: Property.Checkbox({ displayName: 'Response is Binary', - description: - 'Enable for files like PDFs, images, etc. A base64 body will be returned.', + description: 'Return the response as base64, for files like PDFs.', required: false, defaultValue: false, + advanced: true, }), use_proxy: Property.Checkbox({ displayName: 'Use Proxy', defaultValue: false, - description: 'Use a proxy for this request', + description: 'Route this request through an HTTP proxy.', required: false, + advanced: true, }), proxy_settings: Property.DynamicProperties({ auth: PieceAuth.None(), displayName: 'Proxy Settings', refreshers: ['use_proxy'], required: false, - props: async ({ use_proxy }) => { + advanced: true, + props: async ({ use_proxy }): Promise => { if (!use_proxy) return {}; - const fields: DynamicPropsValue = {}; - - fields['proxy_host'] = Property.ShortText({ - displayName: 'Proxy Host', - required: true, - }); - - fields['proxy_port'] = Property.Number({ - displayName: 'Proxy Port', - required: true, - }); - - fields['proxy_username'] = Property.ShortText({ - displayName: 'Proxy Username', - required: false, - }); - - fields['proxy_password'] = Property.ShortText({ - displayName: 'Proxy Password', - required: false, - }); - - return fields; + return { + proxy_host: Property.ShortText({ + displayName: 'Proxy Host', + required: true, + placeholder: 'proxy.example.com', + }), + proxy_port: Property.Number({ + displayName: 'Proxy Port', + required: true, + }), + proxy_username: Property.ShortText({ + displayName: 'Proxy Username', + required: false, + }), + proxy_password: Property.ShortText({ + displayName: 'Proxy Password', + description: 'Stored in the flow and visible in exports.', + required: false, + }), + }; }, }), timeout: Property.Number({ - displayName: 'Timeout(in seconds)', + displayName: 'Timeout', + description: 'Seconds to wait for a response. Empty: up to the flow limit (10 min).', required: false, + min: 1, + advanced: true, }), followRedirects: Property.Checkbox({ displayName: 'Follow redirects', + description: 'Follow 3xx redirects instead of returning the response.', required: false, defaultValue: false, + advanced: true, }), failureMode: Property.StaticDropdown({ displayName: 'On Failure', @@ -261,8 +242,18 @@ export const httpSendRequestAction = createAction({ { label: 'Do not continue (stop the flow)', value: 'continue_none' }, ], }, + advanced: true, }) }, + propertyGroups: [ + { + key: 'request', + display: 'section', + label: 'Request', + icon: 'send', + props: ['method', 'url', 'headers', 'queryParams'], + }, + ], errorHandlingOptions: { continueOnFailure: { hide: true, defaultValue: false }, retryOnFailure: { hide: true, defaultValue: false }, @@ -316,7 +307,6 @@ export const httpSendRequestAction = createAction({ break; } - // Set response type to arraybuffer if binary response is expected if (response_is_binary) { request.responseType = 'arraybuffer'; } diff --git a/packages/pieces/core/http/src/lib/common/props.ts b/packages/pieces/core/http/src/lib/common/props.ts index 1ea7ecbe56fd..c708b6517272 100644 --- a/packages/pieces/core/http/src/lib/common/props.ts +++ b/packages/pieces/core/http/src/lib/common/props.ts @@ -8,6 +8,8 @@ const httpMethodDropdownOptions = Object.values(HttpMethod).map((m) => ({ export const httpMethodDropdown = Property.StaticDropdown({ displayName: 'Method', + description: 'GET reads data, POST creates it.', required: true, + defaultValue: HttpMethod.GET, options: { options: httpMethodDropdownOptions }, });