Repository: chimurai/http-proxy-middleware
Pull request #896 (2023-03-22 09:17:33)

fix(fixRequestBody): support multipart/form-data
The fixRequestBody method provided now does not support the handling of the type for which Content- type is multipart/form- data. This pr has modified the method to support this type.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/58137314/badge)](https://coveralls.io/builds/58137314)

Coverage: 97.662% (+0.04%) from 97.619% when pulling **e0f64c368fa5114b037027f82aa35d50fbb126df on JS-mark:fix/fixRequestBody** into **3e84b15f748d1383aeb095ba34a04b431420c3e5 on chimurai:master**.

chimurai: @JS-mark thank you the contribution and patience.

Currently finalising v3. Will have to find some time to review.  🙏
ajfranzoia: @chimurai any update on this? Thanks
JS-mark: Conflict resolved

Patch:
diff --git a/src/handlers/fix-request-body.ts b/src/handlers/fix-request-body.ts
index 4fe1e45..1286ce5 100644
--- a/src/handlers/fix-request-body.ts
+++ b/src/handlers/fix-request-body.ts
@@ -30,4 +30,23 @@ export function fixRequestBody<TReq = http.IncomingMessage>(
   if (contentType && contentType.includes('application/x-www-form-urlencoded')) {
     writeBody(querystring.stringify(requestBody));
   }
+
+  if (contentType && contentType.includes('multipart/form-data')) {
+    writeBody(handlerFormDataBodyData(contentType, requestBody));
+  }
+}
+
+/**
+ * format FormData data
+ * @param contentType
+ * @param data
+ * @returns
+ */
+function handlerFormDataBodyData(contentType: string, data: any) {
+  const boundary = contentType.replace(/^.*boundary=(.*)$/, '$1');
+  let str = '';
+  for (const [key, value] of Object.entries(data)) {
+    str += `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`;
+  }
+  return str;
 }


Test patch:
diff --git a/test/unit/fix-request-body.spec.ts b/test/unit/fix-request-body.spec.ts
index b176506..2f1b660 100644
--- a/test/unit/fix-request-body.spec.ts
+++ b/test/unit/fix-request-body.spec.ts
@@ -1,7 +1,6 @@
 import { Socket } from 'net';
 import { ClientRequest, IncomingMessage } from 'http';
 import * as querystring from 'querystring';
-
 import { fixRequestBody, BodyParserLikeRequest } from '../../src/handlers/fix-request-body';
 
 const fakeProxyRequest = (): ClientRequest => {
@@ -17,6 +16,15 @@ const createRequestWithBody = (body: unknown): BodyParserLikeRequest => {
   return req;
 };
 
+const handlerFormDataBodyData = (contentType: string, data: { [key: string]: any }) => {
+  const boundary = contentType.replace(/^.*boundary=(.*)$/, '$1');
+  let str = '';
+  for (const [key, value] of Object.entries(data)) {
+    str += `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`;
+  }
+  return str;
+};
+
 describe('fixRequestBody', () => {
   it('should not write when body is undefined', () => {
     const proxyRequest = fakeProxyRequest();
@@ -57,6 +65,55 @@ describe('fixRequestBody', () => {
     expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody);
   });
 
+  it('should write when body is not empty and Content-Type is multipart/form-data', () => {
+    const proxyRequest = fakeProxyRequest();
+    proxyRequest.setHeader('content-type', 'multipart/form-data');
+
+    jest.spyOn(proxyRequest, 'setHeader');
+    jest.spyOn(proxyRequest, 'write');
+
+    fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' }));
+
+    const expectedBody = handlerFormDataBodyData('multipart/form-data', {
+      someField: 'some value',
+    });
+
+    expect(expectedBody).toMatchInlineSnapshot(`
+      "--multipart/form-data
+      Content-Disposition: form-data; name="someField"
+
+      some value
+      "
+    `);
+    expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length);
+    expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody);
+  });
+
+  it('should write when body is not empty and Content-Type includes multipart/form-data', () => {
+    const proxyRequest = fakeProxyRequest();
+    proxyRequest.setHeader('content-type', 'multipart/form-data');
+
+    jest.spyOn(proxyRequest, 'setHeader');
+    jest.spyOn(proxyRequest, 'write');
+
+    fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' }));
+
+    const expectedBody = handlerFormDataBodyData('multipart/form-data', {
+      someField: 'some value',
+    });
+
+    expect(expectedBody).toMatchInlineSnapshot(`
+      "--multipart/form-data
+      Content-Disposition: form-data; name="someField"
+
+      some value
+      "
+    `);
+
+    expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length);
+    expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody);
+  });
+
   it('should write when body is not empty and Content-Type ends with +json', () => {
     const proxyRequest = fakeProxyRequest();
     proxyRequest.setHeader('content-type', 'application/merge-patch+json; charset=utf-8');
@@ -65,7 +122,6 @@ describe('fixRequestBody', () => {
     jest.spyOn(proxyRequest, 'write');
 
     fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' }));
-
     const expectedBody = JSON.stringify({ someField: 'some value' });
     expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length);
     expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody);