Skip to content

Commit e60c371

Browse files
authored
fix: various quirks with how we handle accept headers (#699)
* fix(api): quirk where headers could be duped in body payloads * fix(api): stop sending special headers in body payloads * fix(snippet): stop putting `accept` headers in snippets if we can
1 parent 3368dc2 commit e60c371

8 files changed

Lines changed: 157 additions & 14 deletions

File tree

packages/api/src/core/prepareParams.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ export default async function prepareParams(operation: Operation, body?: unknown
332332
if (typeof metadata === 'object' && !isEmpty(metadata)) {
333333
if (paramName in metadata) {
334334
value = metadata[paramName];
335+
metadataHeaderParam = paramName;
335336
} else if (param.in === 'header') {
336337
// Headers are sent case-insensitive so we need to make sure that we're properly
337338
// matching them when detecting what our incoming payload looks like.
@@ -379,9 +380,7 @@ export default async function prepareParams(operation: Operation, body?: unknown
379380
// If there's any leftover metadata that hasn't been moved into form data for this request we
380381
// need to move it or else it'll get tossed.
381382
if (!isEmpty(metadata)) {
382-
if (operation.isFormUrlEncoded()) {
383-
params.formData = merge(params.formData, metadata);
384-
} else if (typeof metadata === 'object') {
383+
if (typeof metadata === 'object') {
385384
// If the user supplied an `accept` or `authorization` header themselves we should allow it
386385
// through. Normally these headers are automatically handled by `@readme/oas-to-har` but in
387386
// the event that maybe the user wants to return XML for an API that normally returns JSON
@@ -391,8 +390,14 @@ export default async function prepareParams(operation: Operation, body?: unknown
391390
const headerParam = Object.keys(metadata).find(m => m.toLowerCase() === headerName);
392391
if (headerParam) {
393392
params.header[headerName] = metadata[headerParam] as string;
393+
// eslint-disable-next-line no-param-reassign
394+
delete metadata[headerParam];
394395
}
395396
});
397+
}
398+
399+
if (operation.isFormUrlEncoded()) {
400+
params.formData = merge(params.formData, metadata);
396401
} else {
397402
// Any other remaining unused metadata will be unused because we don't know where to place
398403
// it in the request.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "Core",
5+
"description": "All included utility endpoints for Basiq partners",
6+
"version": "3.0.0"
7+
},
8+
"servers": [
9+
{
10+
"url": "https://au-api.basiq.io/"
11+
}
12+
],
13+
"paths": {
14+
"/token": {
15+
"post": {
16+
"tags": ["Authentication"],
17+
"summary": "Generate an auth token",
18+
"description": "Use this endpoint to retrieve a token that will be passed as authorization header for Basiq API",
19+
"operationId": "postToken",
20+
"parameters": [
21+
{
22+
"name": "basiq-version",
23+
"in": "header",
24+
"required": true,
25+
"schema": {
26+
"type": "string",
27+
"example": "3.0"
28+
}
29+
}
30+
],
31+
"requestBody": {
32+
"content": {
33+
"application/x-www-form-urlencoded": {
34+
"schema": {
35+
"properties": {
36+
"scope": {
37+
"type": "string"
38+
},
39+
"userId": {
40+
"type": "string"
41+
}
42+
}
43+
},
44+
"examples": {
45+
"client_access": {
46+
"summary": "For all client side requests",
47+
"value": {
48+
"scope": "CLIENT_ACCESS",
49+
"userId": "6dd30ce4-d4ba-11ec-9d64-0242ac120002"
50+
}
51+
},
52+
"server_access": {
53+
"summary": "For all server side requests",
54+
"value": {
55+
"scope": "SERVER_ACCESS"
56+
}
57+
}
58+
}
59+
}
60+
}
61+
},
62+
"responses": {
63+
"200": {
64+
"description": "OK"
65+
}
66+
},
67+
"security": [
68+
{
69+
"api_key": []
70+
}
71+
]
72+
}
73+
}
74+
},
75+
"components": {
76+
"securitySchemes": {
77+
"api_key": {
78+
"type": "apiKey",
79+
"name": "Authorization",
80+
"in": "header",
81+
"x-default": "Basic NjMxMjNmMWMtZjYxMy00ZjMyLWFiYzUtYzBhZDdhYTY2YmU1OjQ3NWYwMzhkLTBlZmItNGM1ZS1iMzQ0LTAzMzYxOTkyYTRlMw=="
82+
},
83+
"services_token": {
84+
"type": "http",
85+
"scheme": "bearer",
86+
"bearerFormat": "JWT"
87+
}
88+
}
89+
},
90+
"security": [
91+
{
92+
"api_key": []
93+
},
94+
{
95+
"services_token": []
96+
}
97+
],
98+
"x-readme": {
99+
"explorer-enabled": true,
100+
"proxy-enabled": true,
101+
"samples-enabled": true,
102+
"samples-languages": ["curl", "node", "ruby", "javascript", "python"]
103+
}
104+
}

packages/api/test/core/prepareParams.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,41 @@ describe('#prepareParams', () => {
379379
});
380380
});
381381

382+
describe('quirks', () => {
383+
it('should not send special headers in body payloads', async () => {
384+
const basiq = await import('../__fixtures__/definitions/basiq.json').then(Oas.init);
385+
await basiq.dereference();
386+
387+
const operation = basiq.operation('/token', 'post');
388+
389+
await expect(
390+
prepareParams(operation, { scope: 'SERVER_ACCESS' }, { accept: 'application/json', 'BASIQ-version': '3.0' }),
391+
).resolves.toStrictEqual({
392+
formData: {
393+
scope: 'SERVER_ACCESS',
394+
},
395+
header: {
396+
accept: 'application/json',
397+
'basiq-version': '3.0',
398+
},
399+
});
400+
});
401+
402+
it('should not duplicate a supplied header parameter if that header casing matches the spec', async () => {
403+
const basiq = await import('../__fixtures__/definitions/basiq.json').then(Oas.init);
404+
await basiq.dereference();
405+
406+
const operation = basiq.operation('/token', 'post');
407+
408+
await expect(
409+
prepareParams(operation, { scope: 'scope', userId: 'userid' }, { 'basiq-version': '3.0' }),
410+
).resolves.toStrictEqual({
411+
formData: { scope: 'scope', userId: 'userid' },
412+
header: { 'basiq-version': '3.0' },
413+
});
414+
});
415+
});
416+
382417
describe('`accept` header overrides', () => {
383418
it('should support supplying an `accept` header parameter', async () => {
384419
const operation = parameterStyle.operation('/anything/headers', 'get');

packages/httpsnippet-client-api/src/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import type { HttpMethods, OASDocument } from 'oas/dist/rmoas.types';
55

66
import { CodeBuilder } from '@readme/httpsnippet/dist/helpers/code-builder';
77
import contentType from 'content-type';
8-
import Oas from 'oas';
8+
import Oas, { utils } from 'oas';
99
import stringifyObject from 'stringify-object';
1010

11+
const { matchesMimeType } = utils;
12+
1113
// This should really be an exported type in `oas`.
1214
type SecurityType = 'Basic' | 'Bearer' | 'Query' | 'Header' | 'Cookie' | 'OAuth2' | 'http' | 'apiKey';
1315

@@ -224,10 +226,9 @@ const client: Client<APIOptions> = {
224226
return;
225227
}
226228
} else if (headerLower === 'accept') {
227-
// If the `Accept` header here is not the default or first `Accept` header for the
228-
// operations' request body then we should add it otherwise we can let the SDK handle it
229-
// itself.
230-
if (headers[header] === operation.getContentType()) {
229+
// If the `Accept` header here is JSON-like header then we can remove it from the code
230+
// snippet because `api` natively supports and prioritizes JSON over any other mime type.
231+
if (matchesMimeType.json(headers[header] as string)) {
231232
delete headers[header];
232233
return;
233234
}

packages/httpsnippet-client-api/test/__datasets__/full-many-query-params/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const mock: SnippetMock = {
8181
cookie: 'bar-cookie=baz; foo-cookie=bar',
8282
},
8383
functionMatcher: (url, opts) => {
84-
return opts.body === 'foo=bar&foo2=bar2&foo3=bar3&foo4=bar4&accept=application%2Fjson';
84+
return opts.body === 'foo=bar&foo2=bar2&foo3=bar3&foo4=bar4';
8585
},
8686
},
8787
res: {

packages/httpsnippet-client-api/test/__datasets__/full-many-query-params/output.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ sdk.postAnything({
1010
baz: 'abc',
1111
key: 'value',
1212
'bar-cookie': 'baz',
13-
'foo-cookie': 'bar',
14-
accept: 'application/json'
13+
'foo-cookie': 'bar'
1514
})
1615
.then(({ data }) => console.log(data))
1716
.catch(err => console.error(err));

packages/httpsnippet-client-api/test/__datasets__/full/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const mock: SnippetMock = {
6969
cookie: 'bar-cookie=baz; foo-cookie=bar',
7070
},
7171
functionMatcher: (url, opts) => {
72-
return opts.body === 'foo=bar&accept=application%2Fjson';
72+
return opts.body === 'foo=bar';
7373
},
7474
},
7575
res: {

packages/httpsnippet-client-api/test/__datasets__/full/output.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ sdk.postAnything({foo: 'bar'}, {
55
baz: 'abc',
66
key: 'value',
77
'bar-cookie': 'baz',
8-
'foo-cookie': 'bar',
9-
accept: 'application/json'
8+
'foo-cookie': 'bar'
109
})
1110
.then(({ data }) => console.log(data))
1211
.catch(err => console.error(err));

0 commit comments

Comments
 (0)