Skip to content

Commit

Permalink
[CSCwh10631] Form data content types should not provide JSON option a…
Browse files Browse the repository at this point in the history
…nd should properly encode payload as query parameters instead of JSON-like string converted into Form Data
  • Loading branch information
mahamdan committed Nov 30, 2023
1 parent 384a6d9 commit 726de87
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 15 deletions.
58 changes: 50 additions & 8 deletions src/components/Operation/Operation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import {
} from '../../common-elements';
import { OperationModel } from '../../services/models';
import styled from '../../styled-components';
import { appendParamsToPath, mapStatusCodeToType, setCookieParams } from '../../utils/tryout';
import {
appendParamsToPath,
entriesToQueryString,
mapStatusCodeToType,
setCookieParams,
} from '../../utils/tryout';
import { CallbacksList } from '../Callbacks';
import { Endpoint } from '../Endpoint/Endpoint';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
Expand Down Expand Up @@ -75,33 +80,70 @@ export class Operation extends React.Component<OperationProps, OperationState> {
* defines param location as being one of 'path', 'query', 'cookie' or 'header', while
* fetch API defines request as having RequestInit type, which has 'headers' as a member field
*/
handleApiCall = ({ queryParams, pathParams, cookieParams, header: headers, body }: Request) => {
handleApiCall = ({
queryParams,
pathParams,
cookieParams,
header: headers,
body = null,
}: Request) => {
const {
operation: { httpVerb, path, requestBody },
} = this.props;

const requestBodyContent = requestBody?.content;
const activeMimeIdx = requestBodyContent?.activeMimeIdx;
const contentType =
activeMimeIdx !== undefined && requestBodyContent?.mediaTypes[activeMimeIdx]?.name;
activeMimeIdx === undefined ? undefined : requestBodyContent?.mediaTypes[activeMimeIdx]?.name;

const isFormData = contentType === 'multipart/form-data';

if (!isFormData) {
headers = { 'Content-Type': contentType || 'application/json', ...headers };
}

const formData = new FormData();
if (isFormData && body && typeof body === 'object') {
Object.entries(body as any).forEach(([key, value]) => {
const getFormDataWithObjectEntriesAppended = (
object: Record<string, any>,
formData: FormData,
) => {
if (!object || typeof object !== 'object' || !formData) {
return formData;
}
Object.entries(object as any).forEach(([key, value]) => {
const isFileValue = (value as any) instanceof File;
const isJsonValue = !isFileValue && typeof value === 'object' && value !== null;
formData.append(
key,
isFileValue ? (value as any) : isJsonValue ? JSON.stringify(value)! : value,
);
});
}
return formData;
};

const getFormDataFromObject = (object: Record<string, any>) => {
const formData = new FormData();
return getFormDataWithObjectEntriesAppended(object, formData);
};

const getQueryStringFromObject = (object: Record<string, any>) => {
const entries = Object.entries(object as any);
return entriesToQueryString(entries);
};

const getBodyByContentType = (body: BodyInit | null, contentType: string | undefined): any => {
if (typeof body === 'string' || body === null) {
return body;
}
const isFormData = contentType === 'multipart/form-data';
const isEncodedFormContent = contentType === 'application/x-www-form-urlencoded';
if (isFormData) {
return getFormDataFromObject(body);
}
if (isEncodedFormContent) {
return getQueryStringFromObject(body);
}
return JSON.stringify(body);
};

const request: RequestInit =
Object.values(NoRequestBodyHttpVerb)
Expand All @@ -114,7 +156,7 @@ export class Operation extends React.Component<OperationProps, OperationState> {
: {
method: httpVerb,
headers,
body: typeof body === 'string' ? body : isFormData ? formData : JSON.stringify(body),
body: getBodyByContentType(body, contentType),
};

setCookieParams(cookieParams);
Expand Down
6 changes: 5 additions & 1 deletion src/components/TryOut/Body.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { toLower } from 'lodash';

import { RequestBodyModel } from '../../services';
import { JsonViewer } from '../JsonViewer/JsonViewer';
Expand Down Expand Up @@ -35,6 +36,9 @@ export const Body = ({
requestPayload,
}: BodyProps) => {
if (!specBody) return null;
const contentType = toLower(specBody?.content?.active?.name);
const FORM_DATA_CONTENT_TYPES = ['multipart/form-data', 'application/x-www-form-urlencoded'];
const shouldEnableJsonOption = !FORM_DATA_CONTENT_TYPES.includes(contentType);

return (
<>
Expand All @@ -49,7 +53,7 @@ export const Body = ({
cursor={'pointer'}
onChange={({ target: { value } }) => setIsFormData(value === 'form-data')}
>
<option value="raw-json">JSON</option>
{shouldEnableJsonOption && <option value="raw-json">JSON</option>}
<option value="form-data">Form</option>
</Dropdown>
</SectionHeader>
Expand Down
11 changes: 6 additions & 5 deletions src/components/TryOut/TryOut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ export const TryOut = observer(
header: {},
body: getInitialBodyByOperation(operation),
});
const [isFormData, setIsFormData] = React.useState(false);

const contentType = toLower(operation?.requestBody?.content?.active?.name);
const isFormDataContent = contentType === 'multipart/form-data';
const isEncodedFormContent = contentType === 'application/x-www-form-urlencoded';

const [isFormData, setIsFormData] = React.useState(isFormDataContent || isEncodedFormContent);
const [error, setError] = React.useState(undefined);
const [showError, setShowError] = React.useState(false);
const [requiredFields, setRequiredFields] = React.useState<RequiredField[]>(
Expand Down Expand Up @@ -304,11 +309,7 @@ export const TryOut = observer(
const pathParams = operation.parameters?.filter(param => param.in === 'path');
const cookieParams = operation.parameters?.filter(param => param.in === 'cookie');

const contentType = toLower(operation?.requestBody?.content?.active?.name);
const isJsonContent = contentType === 'application/json';
const isFormDataContent = contentType === 'multipart/form-data';
const isEncodedFormContent = contentType === 'application/x-www-form-urlencoded';

const schemaType = operation.requestBody?.content?.mediaTypes[0]?.schema?.type;

const onHeaderChange = (fieldName, value, arrayIndex, ancestors, location) =>
Expand Down
2 changes: 1 addition & 1 deletion src/utils/tryout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const appendPathParamsToPath = (path: string, pathParams: Record<string, string>
* ]
* becomes "a=b&c=d"
*/
const entriesToQueryString = (entries): string => {
export const entriesToQueryString = (entries): string => {
let queryString = '';
for (let i = 0; i < entries.length; i++) {
const [key, value] = entries[i];
Expand Down

0 comments on commit 726de87

Please sign in to comment.