mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Prevent duplicate Content-Type headers in OpenAPI code samples (#3924)
This commit is contained in:
@@ -10,6 +10,7 @@ import { type OpenAPIContext, getOpenAPIClientContext } from './context';
|
||||
import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import type { OpenAPIOperationData } from './types';
|
||||
import { mergeHeaders } from './util/headers';
|
||||
import { getDefaultServerURL } from './util/server';
|
||||
import {
|
||||
resolvePrefillCodePlaceholderFromSecurityScheme,
|
||||
@@ -125,10 +126,9 @@ function generateCodeSamples(props: {
|
||||
const mediaTypeRendererFactories = Object.entries(requestBody?.content ?? {}).map(
|
||||
([mediaType, mediaTypeObject]) => {
|
||||
return (generator: CodeSampleGenerator) => {
|
||||
const mediaTypeHeaders = {
|
||||
...genericHeaders,
|
||||
const mediaTypeHeaders = mergeHeaders(genericHeaders, {
|
||||
'Content-Type': mediaType,
|
||||
};
|
||||
});
|
||||
return {
|
||||
mediaType,
|
||||
element: context.renderCodeBlock({
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { mergeHeaders } from './headers';
|
||||
|
||||
describe('mergeHeaders', () => {
|
||||
it('should merge headers without conflicts', () => {
|
||||
const baseHeaders = {
|
||||
'user-agent': 'browser',
|
||||
Authorization: 'Bearer token',
|
||||
};
|
||||
const overrideHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const result = mergeHeaders(baseHeaders, overrideHeaders);
|
||||
|
||||
expect(result['user-agent']).toBe('browser');
|
||||
expect(result.Authorization).toBe('Bearer token');
|
||||
expect(result['Content-Type']).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should remove duplicate headers case-insensitively', () => {
|
||||
const baseHeaders = {
|
||||
'content-type': 'application/vnd.api+json',
|
||||
'user-agent': 'browser',
|
||||
};
|
||||
const overrideHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const result = mergeHeaders(baseHeaders, overrideHeaders);
|
||||
|
||||
// Should only have one Content-Type header (from override)
|
||||
expect(result['Content-Type']).toBe('application/json');
|
||||
expect(result['content-type']).toBeUndefined();
|
||||
expect(result['user-agent']).toBe('browser');
|
||||
});
|
||||
|
||||
it('should handle multiple case variations', () => {
|
||||
const baseHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'content-type': 'application/vnd.api+json',
|
||||
'CONTENT-TYPE': 'text/plain',
|
||||
'User-Agent': 'browser',
|
||||
};
|
||||
const overrideHeaders = {
|
||||
'Content-Type': 'application/xml',
|
||||
};
|
||||
|
||||
const result = mergeHeaders(baseHeaders, overrideHeaders);
|
||||
|
||||
// Should only have one Content-Type header (from override)
|
||||
expect(result['Content-Type']).toBe('application/xml');
|
||||
expect(result['content-type']).toBeUndefined();
|
||||
expect(result['CONTENT-TYPE']).toBeUndefined();
|
||||
expect(result['User-Agent']).toBe('browser');
|
||||
});
|
||||
|
||||
it('should override headers with same key but different casing', () => {
|
||||
const baseHeaders = {
|
||||
'user-agent': 'old-browser',
|
||||
'User-Agent': 'new-browser',
|
||||
};
|
||||
const overrideHeaders = {
|
||||
'USER-AGENT': 'override-browser',
|
||||
};
|
||||
|
||||
const result = mergeHeaders(baseHeaders, overrideHeaders);
|
||||
|
||||
// Should only have the override header
|
||||
expect(result['USER-AGENT']).toBe('override-browser');
|
||||
expect(result['user-agent']).toBeUndefined();
|
||||
expect(result['User-Agent']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty base headers', () => {
|
||||
const baseHeaders = {};
|
||||
const overrideHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer token',
|
||||
};
|
||||
|
||||
const result = mergeHeaders(baseHeaders, overrideHeaders);
|
||||
|
||||
expect(result['Content-Type']).toBe('application/json');
|
||||
expect(result.Authorization).toBe('Bearer token');
|
||||
});
|
||||
|
||||
it('should handle empty override headers', () => {
|
||||
const baseHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer token',
|
||||
};
|
||||
const overrideHeaders = {};
|
||||
|
||||
const result = mergeHeaders(baseHeaders, overrideHeaders);
|
||||
|
||||
expect(result['Content-Type']).toBe('application/json');
|
||||
expect(result.Authorization).toBe('Bearer token');
|
||||
});
|
||||
|
||||
it('should preserve override header casing', () => {
|
||||
const baseHeaders = {
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
const overrideHeaders = {
|
||||
'Content-Type': 'application/xml',
|
||||
};
|
||||
|
||||
const result = mergeHeaders(baseHeaders, overrideHeaders);
|
||||
|
||||
// Should use the casing from override headers
|
||||
expect(result['Content-Type']).toBe('application/xml');
|
||||
expect(Object.keys(result)).toContain('Content-Type');
|
||||
expect(Object.keys(result)).not.toContain('content-type');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Merges two header objects, removing duplicate headers case-insensitively.
|
||||
* Headers from the second object take precedence over headers from the first object.
|
||||
*/
|
||||
export function mergeHeaders(
|
||||
baseHeaders: Record<string, string>,
|
||||
overrideHeaders: Record<string, string>
|
||||
): Record<string, string> {
|
||||
const overrideKeysLower = new Set(Object.keys(overrideHeaders).map((key) => key.toLowerCase()));
|
||||
const filteredBaseHeaders = Object.fromEntries(
|
||||
Object.entries(baseHeaders).filter(([key]) => !overrideKeysLower.has(key.toLowerCase()))
|
||||
);
|
||||
|
||||
return {
|
||||
...filteredBaseHeaders,
|
||||
...overrideHeaders,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user