Skip to content

Commit a210754

Browse files
authored
feat(api): surface Warning response headers (#721)
* feat(api): surface `Warning` response headers * chore: var names and docs * docs: add MDN link
1 parent 9dc36c1 commit a210754

File tree

3 files changed

+141
-3
lines changed

3 files changed

+141
-3
lines changed

__tests__/lib/fetch.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,80 @@ describe('#fetch()', () => {
106106
mock.done();
107107
});
108108

109+
describe('warning response header', () => {
110+
let consoleWarnSpy;
111+
112+
const getWarningCommandOutput = () => {
113+
return [consoleWarnSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n');
114+
};
115+
116+
beforeEach(() => {
117+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
118+
});
119+
120+
afterEach(() => {
121+
consoleWarnSpy.mockRestore();
122+
});
123+
124+
it('should not log anything if no warning header was passed', async () => {
125+
const mock = getAPIMock().get('/api/v1/some-warning').reply(200, undefined, {
126+
Warning: '',
127+
});
128+
129+
await fetch(`${config.get('host')}/api/v1/some-warning`);
130+
131+
// eslint-disable-next-line no-console
132+
expect(console.warn).toHaveBeenCalledTimes(0);
133+
expect(getWarningCommandOutput()).toBe('');
134+
135+
mock.done();
136+
});
137+
138+
it('should surface a single warning header', async () => {
139+
const mock = getAPIMock().get('/api/v1/some-warning').reply(200, undefined, {
140+
Warning: '199 - "some error"',
141+
});
142+
143+
await fetch(`${config.get('host')}/api/v1/some-warning`);
144+
145+
// eslint-disable-next-line no-console
146+
expect(console.warn).toHaveBeenCalledTimes(1);
147+
expect(getWarningCommandOutput()).toBe('⚠️ ReadMe API Warning: some error');
148+
149+
mock.done();
150+
});
151+
152+
it('should surface multiple warning headers', async () => {
153+
const mock = getAPIMock().get('/api/v1/some-warning').reply(200, undefined, {
154+
Warning: '199 - "some error" 199 - "another error"',
155+
});
156+
157+
await fetch(`${config.get('host')}/api/v1/some-warning`);
158+
159+
// eslint-disable-next-line no-console
160+
expect(console.warn).toHaveBeenCalledTimes(2);
161+
expect(getWarningCommandOutput()).toBe(
162+
'⚠️ ReadMe API Warning: some error\n\n⚠️ ReadMe API Warning: another error'
163+
);
164+
165+
mock.done();
166+
});
167+
168+
it('should surface header content even if parsing fails', async () => {
169+
const mock = getAPIMock().get('/api/v1/some-warning').reply(200, undefined, {
170+
Warning: 'some garbage error',
171+
});
172+
173+
await fetch(`${config.get('host')}/api/v1/some-warning`);
174+
175+
// eslint-disable-next-line no-console
176+
expect(console.warn).toHaveBeenCalledTimes(1);
177+
expect(getWarningCommandOutput()).toBe('⚠️ ReadMe API Warning: some garbage error');
178+
179+
mock.done();
180+
});
181+
});
182+
109183
describe('proxies', () => {
110184
afterEach(() => {
111185
delete process.env.https_proxy;

src/lib/fetch.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import pkg from '../../package.json';
88

99
import APIError from './apiError';
1010
import { isGHA } from './isCI';
11-
import { debug } from './logger';
11+
import { debug, warn } from './logger';
1212

1313
const SUCCESS_NO_CONTENT = 204;
1414

@@ -22,6 +22,58 @@ function getProxy() {
2222
return '';
2323
}
2424

25+
/**
26+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning}
27+
* @see {@link https://www.rfc-editor.org/rfc/rfc7234#section-5.5}
28+
* @see {@link https://github.com/marcbachmann/warning-header-parser}
29+
*/
30+
interface WarningHeader {
31+
code: string;
32+
agent: string;
33+
message: string;
34+
date?: string;
35+
}
36+
37+
function stripQuotes(s: string) {
38+
if (!s) return '';
39+
return s.replace(/(^"|[",]*$)/g, '');
40+
}
41+
42+
/**
43+
* Parses Warning header into an array of warning header objects
44+
* @param header raw `Warning` header
45+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning}
46+
* @see {@link https://www.rfc-editor.org/rfc/rfc7234#section-5.5}
47+
* @see {@link https://github.com/marcbachmann/warning-header-parser}
48+
*/
49+
function parseWarningHeader(header: string): WarningHeader[] {
50+
try {
51+
const warnings = header.split(/([0-9]{3} [a-z0-9.@\-/]*) /g);
52+
53+
let previous: WarningHeader;
54+
55+
return warnings.reduce((all, w) => {
56+
// eslint-disable-next-line no-param-reassign
57+
w = w.trim();
58+
const newError = w.match(/^([0-9]{3}) (.*)/);
59+
if (newError) {
60+
previous = { code: newError[1], agent: newError[2], message: '' };
61+
} else if (w) {
62+
const errorContent = w.split(/" "/);
63+
if (errorContent) {
64+
previous.message = stripQuotes(errorContent[0]);
65+
previous.date = stripQuotes(errorContent[1]);
66+
all.push(previous);
67+
}
68+
}
69+
return all;
70+
}, []);
71+
} catch (e) {
72+
debug(`error parsing warning header: ${e.message}`);
73+
return [{ code: '199', agent: '-', message: header }];
74+
}
75+
}
76+
2577
/**
2678
* Getter function for a string to be used in the user-agent header based on the current
2779
* environment.
@@ -64,6 +116,16 @@ export default function fetch(url: string, options: RequestInit = { headers: new
64116
return nodeFetch(fullUrl, {
65117
...options,
66118
headers,
119+
}).then(res => {
120+
const warningHeader = res.headers.get('Warning');
121+
if (warningHeader) {
122+
debug(`received warning header: ${warningHeader}`);
123+
const warnings = parseWarningHeader(warningHeader);
124+
warnings.forEach(warning => {
125+
warn(warning.message, 'ReadMe API Warning:');
126+
});
127+
}
128+
return res;
67129
});
68130
}
69131

src/lib/logger.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,14 @@ function oraOptions() {
6868

6969
/**
7070
* Wrapper for warn statements.
71+
* @param prefix Text that precedes the warning.
72+
* This is *not* used in the GitHub Actions-formatted warning.
7173
*/
72-
function warn(input: string) {
74+
function warn(input: string, prefix = 'Warning!') {
7375
/* istanbul ignore next */
7476
if (isGHA() && !isTest()) return core.warning(input);
7577
// eslint-disable-next-line no-console
76-
return console.warn(chalk.yellow(`⚠️ Warning! ${input}`));
78+
return console.warn(chalk.yellow(`⚠️ ${prefix} ${input}`));
7779
}
7880

7981
export { debug, error, info, oraOptions, warn };

0 commit comments

Comments
 (0)