Skip to content

Replace deprecated undici formData() method with @fastify/busboy implementation #355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: v4.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/http/HttpRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -96,8 +97,7 @@ export class HttpRequest implements types.HttpRequest {
}

async formData(): Promise<FormData> {
// eslint-disable-next-line deprecation/deprecation
return this.#uReq.formData();
return parseFormData(this.#uReq.body, this.#uReq.headers);
}

async json(): Promise<unknown> {
Expand Down
4 changes: 2 additions & 2 deletions src/http/HttpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,8 +64,7 @@ export class HttpResponse implements types.HttpResponse {
}

async formData(): Promise<FormData> {
// eslint-disable-next-line deprecation/deprecation
return this.#uRes.formData();
return parseFormData(this.#uRes.body, this.#uRes.headers);
}

async json(): Promise<unknown> {
Expand Down
99 changes: 99 additions & 0 deletions src/http/formDataParser.ts
Original file line number Diff line number Diff line change
@@ -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<any> | null, headers: Headers): Promise<FormData> {
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);
});
}
4 changes: 2 additions & 2 deletions types/InvocationContext.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion types/input.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion types/output.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';

/**
Expand Down