Skip to content

Commit

Permalink
Add Content-Type and Accept headers where necessary (stoplightio#2349)
Browse files Browse the repository at this point in the history
* feat(elements-core): remove content-type header for empty bodies

* feat(elements-core): add accept header
  • Loading branch information
provokateurin authored Apr 17, 2023
1 parent bb0e16a commit bbe782c
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 16 deletions.
12 changes: 3 additions & 9 deletions packages/elements-core/src/components/TryIt/TryIt.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('TryIt', () => {
const requestInit = fetchMock.mock.calls[0][1]!;
expect(requestInit.method).toMatch(/^get$/i);
const headers = new Headers(requestInit.headers);
expect(headers.get('Content-Type')).toBe('application/json');
expect(headers.get('Content-Type')).toBe(null);
});

it('uses cors proxy url, if provided', async () => {
Expand Down Expand Up @@ -722,7 +722,6 @@ describe('TryIt', () => {
expect.objectContaining({
method: 'GET',
headers: {
'Content-Type': 'application/json',
Prefer: 'code=200',
},
}),
Expand All @@ -731,9 +730,7 @@ describe('TryIt', () => {
'https://todos.stoplight.io/todos',
expect.objectContaining({
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
headers: {},
}),
],
]);
Expand All @@ -760,9 +757,7 @@ describe('TryIt', () => {
'https://mock-todos.stoplight.io/todos',
expect.objectContaining({
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
headers: {},
}),
],
]);
Expand Down Expand Up @@ -823,7 +818,6 @@ describe('TryIt', () => {
expect.objectContaining({
method: 'GET',
headers: {
'Content-Type': 'application/json',
Prefer: 'code=200',
},
}),
Expand Down
104 changes: 103 additions & 1 deletion packages/elements-core/src/components/TryIt/build-request.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IHttpOperation } from '@stoplight/types';

import { operation as minimalOperation } from '../../__fixtures__/operations/operation-minimal';
import httpOperation from '../../__fixtures__/operations/operation-parameters';
import { getQueryParams } from './build-request';
import { getAcceptedMimeTypes, getQueryParams } from './build-request';

describe('Build Request', () => {
describe('Query params', () => {
Expand Down Expand Up @@ -105,4 +107,104 @@ describe('Build Request', () => {
]);
});
});

describe('getAcceptedMimeTypes', () => {
const operationSingleResponse: IHttpOperation = {
id: 'adsf',
method: 'GET',
path: '/a/path',
responses: [
{
id: 'responseA',
code: '200',
contents: [
{
id: 'adsf',
mediaType: 'application/json',
},
{
id: 'dfvs',
mediaType: 'application/json',
},
{
id: 'aaaa',
mediaType: 'multipart/form-data',
},
],
},
],
};

const operationMultipleResponses: IHttpOperation = {
id: 'sfdf',
method: 'POST',
path: '/a/nother/path',
responses: [
{
id: 'responseA',
code: '200',
contents: [
{
id: 'adsf',
mediaType: 'application/json',
},
{
id: 'dfvs',
mediaType: 'application/json',
},
],
},
{
id: 'responseB',
code: '200',
contents: [
{
id: 'adsf',
mediaType: 'application/json',
},
{
id: 'dfvs',
mediaType: 'multipart/form-data',
},
],
},
{
id: 'responseC',
code: '200',
contents: [
{
id: 'dfvs',
mediaType: 'multipart/form-data',
},
],
},
{
id: 'responseB',
code: '200',
contents: [
{
id: 'adsf',
mediaType: 'text/json',
},
{
id: 'dfvs',
mediaType: 'multipart/form-data',
},
],
},
],
};

it('Handles a single response with duplicates', () => {
expect(getAcceptedMimeTypes(operationSingleResponse)).toStrictEqual(['application/json', 'multipart/form-data']);
});

it('Handles multiple responses and dedups appropriately', () => {
expect(getAcceptedMimeTypes(operationMultipleResponses)).toStrictEqual([
'application/json',
'multipart/form-data',
'text/json',
]);
});
});
});
38 changes: 32 additions & 6 deletions packages/elements-core/src/components/TryIt/build-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ export async function buildFetchRequest({
}: BuildRequestInput): Promise<Parameters<typeof fetch>> {
const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy });

const shouldIncludeBody = ['PUT', 'POST', 'PATCH'].includes(httpOperation.method.toUpperCase());
const shouldIncludeBody =
['PUT', 'POST', 'PATCH'].includes(httpOperation.method.toUpperCase()) && bodyInput !== undefined;

const queryParams = getQueryParams({ httpOperation, parameterValues });

Expand All @@ -150,11 +151,14 @@ export async function buildFetchRequest({

const body = typeof bodyInput === 'object' ? await createRequestBody(mediaTypeContent, bodyInput) : bodyInput;

const acceptedMimeTypes = getAcceptedMimeTypes(httpOperation);
const headers = {
...(acceptedMimeTypes.length > 0 && { Accept: acceptedMimeTypes.join(', ') }),
// do not include multipart/form-data - browser handles its content type and boundary
...(mediaTypeContent?.mediaType !== 'multipart/form-data' && {
'Content-Type': mediaTypeContent?.mediaType ?? 'application/json',
}),
...(mediaTypeContent?.mediaType !== 'multipart/form-data' &&
shouldIncludeBody && {
'Content-Type': mediaTypeContent?.mediaType ?? 'application/json',
}),
...Object.fromEntries(headersWithAuth.map(nameAndValueObjectToPair)),
...mockData?.header,
};
Expand Down Expand Up @@ -240,7 +244,8 @@ export async function buildHarRequest({
const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy });

const mimeType = mediaTypeContent?.mediaType ?? 'application/json';
const shouldIncludeBody = ['PUT', 'POST', 'PATCH'].includes(httpOperation.method.toUpperCase());
const shouldIncludeBody =
['PUT', 'POST', 'PATCH'].includes(httpOperation.method.toUpperCase()) && bodyInput !== undefined;

const queryParams = getQueryParams({ httpOperation, parameterValues });

Expand All @@ -252,6 +257,15 @@ export async function buildHarRequest({
headerParams.push({ name: 'Prefer', value: mockData.header.Prefer });
}

if (shouldIncludeBody) {
headerParams.push({ name: 'Content-Type', value: mimeType });
}

const acceptedMimeTypes = getAcceptedMimeTypes(httpOperation);
if (acceptedMimeTypes.length > 0) {
headerParams.push({ name: 'Accept', value: acceptedMimeTypes.join(', ') });
}

const [queryParamsWithAuth, headerParamsWithAuth] = runAuthRequestEhancements(auth, queryParams, headerParams);
const expandedPath = uriExpand(httpOperation.path, parameterValues);
const urlObject = new URL(serverUrl + expandedPath);
Expand Down Expand Up @@ -284,7 +298,7 @@ export async function buildHarRequest({
url: urlObject.href,
httpVersion: 'HTTP/1.1',
cookies: [],
headers: [{ name: 'Content-Type', value: mimeType }, ...headerParamsWithAuth],
headers: headerParamsWithAuth,
queryString: queryParamsWithAuth,
postData: postData,
headersSize: -1,
Expand All @@ -300,3 +314,15 @@ function uriExpand(uri: string, data: Dictionary<string, string>) {
return data[value] || value;
});
}

export function getAcceptedMimeTypes(httpOperation: IHttpOperation): string[] {
return Array.from(
new Set(
httpOperation.responses.flatMap(response =>
response === undefined || response.contents === undefined
? []
: response.contents.map(contentType => contentType.mediaType),
),
),
);
}

0 comments on commit bbe782c

Please sign in to comment.