Skip to content
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

[CSCwh06165][CSCwh10631] Form data content types should not provide JSON option and should properly encode payload as query parameters instead of JSON-like string converted into Form Data #66

Merged
merged 1 commit into from
Feb 8, 2024
Merged
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
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
Loading