diff --git a/package-lock.json b/package-lock.json index 4f216eb..618241a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "4.7.3", "license": "MIT", "dependencies": { + "@fastify/busboy": "^3.1.1", "cookie": "^0.7.0", "long": "^4.0.0", "undici": "^7.10.0" }, "devDependencies": { + "@types/busboy": "^1.5.4", "@types/chai": "^4.2.22", "@types/chai-as-promised": "^7.1.5", "@types/cookie": "^0.6.0", @@ -239,6 +241,12 @@ "node": ">= 4" } }, + "node_modules/@fastify/busboy": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", + "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==", + "license": "MIT" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -430,6 +438,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/busboy": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", + "integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "4.3.20", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", diff --git a/package.json b/package.json index 718da4f..50b75db 100644 --- a/package.json +++ b/package.json @@ -41,11 +41,13 @@ "watch": "webpack --watch --mode development" }, "dependencies": { + "@fastify/busboy": "^3.1.1", "cookie": "^0.7.0", "long": "^4.0.0", "undici": "^7.10.0" }, "devDependencies": { + "@types/busboy": "^1.5.4", "@types/chai": "^4.2.22", "@types/chai-as-promised": "^7.1.5", "@types/cookie": "^0.6.0", @@ -65,8 +67,8 @@ "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.29.0", "eslint-plugin-prettier": "^4.0.0", - "eslint-webpack-plugin": "^3.2.0", "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-webpack-plugin": "^3.2.0", "fork-ts-checker-webpack-plugin": "^7.2.13", "fs-extra": "^10.0.1", "globby": "^11.0.0", diff --git a/src/http/HttpRequest.ts b/src/http/HttpRequest.ts index f19df17..d8bd0f7 100644 --- a/src/http/HttpRequest.ts +++ b/src/http/HttpRequest.ts @@ -15,6 +15,7 @@ import { fromRpcTypedData } from '../converters/fromRpcTypedData'; import { AzFuncSystemError } from '../errors'; import { isDefined, nonNullProp } from '../utils/nonNull'; import { extractHttpUserFromHeaders } from './extractHttpUserFromHeaders'; +import { parseFormData } from './formDataParser'; interface InternalHttpRequestInit extends RpcHttpData { undiciRequest?: uRequest; @@ -96,8 +97,7 @@ export class HttpRequest implements types.HttpRequest { } async formData(): Promise { - // eslint-disable-next-line deprecation/deprecation - return this.#uReq.formData(); + return parseFormData(this.#uReq.body, this.#uReq.headers); } async json(): Promise { diff --git a/src/http/HttpResponse.ts b/src/http/HttpResponse.ts index a71d43c..c2e380f 100644 --- a/src/http/HttpResponse.ts +++ b/src/http/HttpResponse.ts @@ -7,6 +7,7 @@ import { Blob } from 'buffer'; import { ReadableStream } from 'stream/web'; import { FormData, Headers, Response as uResponse, ResponseInit as uResponseInit } from 'undici'; import { isDefined } from '../utils/nonNull'; +import { parseFormData } from './formDataParser'; interface InternalHttpResponseInit extends HttpResponseInit { undiciResponse?: uResponse; @@ -63,8 +64,7 @@ export class HttpResponse implements types.HttpResponse { } async formData(): Promise { - // eslint-disable-next-line deprecation/deprecation - return this.#uRes.formData(); + return parseFormData(this.#uRes.body, this.#uRes.headers); } async json(): Promise { diff --git a/src/http/formDataParser.ts b/src/http/formDataParser.ts new file mode 100644 index 0000000..c32d70d --- /dev/null +++ b/src/http/formDataParser.ts @@ -0,0 +1,99 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import { Readable } from 'node:stream'; +import { Busboy } from '@fastify/busboy'; +import { ReadableStream } from 'stream/web'; +import { FormData, Headers } from 'undici'; + +/** + * Parse form data from a ReadableStream using @fastify/busboy as recommended by undici + * This replaces the deprecated formData() method from undici + */ +export async function parseFormData(body: ReadableStream | null, headers: Headers): Promise { + if (!body) { + throw new TypeError('Cannot parse form data from null body'); + } + + const contentType = headers.get('content-type'); + if (!contentType) { + throw new TypeError('Content-Type header is required for form data parsing'); + } + + // Check if content type is supported + const isMultipart = contentType.includes('multipart/form-data'); + const isUrlEncoded = contentType.includes('application/x-www-form-urlencoded'); + + if (!isMultipart && !isUrlEncoded) { + throw new TypeError( + `Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".` + ); + } + + // For URL-encoded data, we can parse it directly + if (isUrlEncoded) { + const readable = Readable.fromWeb(body); + const chunks: Buffer[] = []; + + for await (const chunk of readable) { + if (Buffer.isBuffer(chunk)) { + chunks.push(chunk); + } else if (chunk instanceof Uint8Array) { + chunks.push(Buffer.from(chunk)); + } else { + chunks.push(Buffer.from(String(chunk))); + } + } + + const buffer = Buffer.concat(chunks); + const text = buffer.toString('utf-8'); + const formData = new FormData(); + + const params = new URLSearchParams(text); + for (const [key, value] of params) { + formData.append(key, value); + } + + return formData; + } + + // For multipart data, use busboy + return new Promise((resolve, reject) => { + const formData = new FormData(); + const readable = Readable.fromWeb(body); + + const busboy = new Busboy({ + headers: { 'content-type': contentType }, + }); + + busboy.on('field', (fieldname, value) => { + formData.append(fieldname, value); + }); + + busboy.on('file', (fieldname, fileStream, filename, encoding, mimeType) => { + const chunks: Uint8Array[] = []; + + fileStream.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + + fileStream.on('end', () => { + const buffer = Buffer.concat(chunks); + const file = new File([buffer], filename || 'unknown', { + type: mimeType || 'application/octet-stream', + }); + formData.append(fieldname, file); + }); + + fileStream.on('error', reject); + }); + + busboy.on('error', reject); + + busboy.on('finish', () => { + resolve(formData); + }); + + readable.pipe(busboy); + }); +} diff --git a/types/InvocationContext.d.ts b/types/InvocationContext.d.ts index 8815bdf..c991557 100644 --- a/types/InvocationContext.d.ts +++ b/types/InvocationContext.d.ts @@ -129,7 +129,7 @@ export interface InvocationContextExtraInputs { * @input the configuration object for this SQL input */ get(input: SqlInput): unknown; - + /** * Get a secondary MySql items input for this invocation * @input the configuration object for this MySql input @@ -223,7 +223,7 @@ export interface InvocationContextExtraOutputs { * @message the output event(s) value */ set(output: EventGridOutput, events: EventGridPartialEvent | EventGridPartialEvent[]): void; - + /** * Set a secondary MySql items output for this invocation * @output the configuration object for this MySql output diff --git a/types/input.d.ts b/types/input.d.ts index 52d8c68..b264405 100644 --- a/types/input.d.ts +++ b/types/input.d.ts @@ -4,10 +4,10 @@ import { CosmosDBInput, CosmosDBInputOptions } from './cosmosDB'; import { GenericInputOptions } from './generic'; import { FunctionInput } from './index'; +import { MySqlInput, MySqlInputOptions } from './mySql'; import { SqlInput, SqlInputOptions } from './sql'; import { StorageBlobInput, StorageBlobInputOptions } from './storage'; import { TableInput, TableInputOptions } from './table'; -import { MySqlInput, MySqlInputOptions } from './mySql'; import { WebPubSubConnectionInput, WebPubSubConnectionInputOptions, diff --git a/types/output.d.ts b/types/output.d.ts index b9d9d83..e9e08d9 100644 --- a/types/output.d.ts +++ b/types/output.d.ts @@ -7,6 +7,7 @@ import { EventHubOutput, EventHubOutputOptions } from './eventHub'; import { GenericOutputOptions } from './generic'; import { HttpOutput, HttpOutputOptions } from './http'; import { FunctionOutput } from './index'; +import { MySqlOutput, MySqlOutputOptions } from './mySql'; import { ServiceBusQueueOutput, ServiceBusQueueOutputOptions, @@ -16,7 +17,6 @@ import { import { SqlOutput, SqlOutputOptions } from './sql'; import { StorageBlobOutput, StorageBlobOutputOptions, StorageQueueOutput, StorageQueueOutputOptions } from './storage'; import { TableOutput, TableOutputOptions } from './table'; -import { MySqlOutput, MySqlOutputOptions } from './mySql'; import { WebPubSubOutput, WebPubSubOutputOptions } from './webpubsub'; /**