Skip to content

Commit

Permalink
feat(api): surface Warning response headers (#721)
Browse files Browse the repository at this point in the history
* feat(api): surface `Warning` response headers

* chore: var names and docs

* docs: add MDN link
  • Loading branch information
kanadgupta authored Dec 20, 2022
1 parent 9dc36c1 commit a210754
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 3 deletions.
74 changes: 74 additions & 0 deletions __tests__/lib/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,80 @@ describe('#fetch()', () => {
mock.done();
});

describe('warning response header', () => {
let consoleWarnSpy;

const getWarningCommandOutput = () => {
return [consoleWarnSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n');
};

beforeEach(() => {
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
});

afterEach(() => {
consoleWarnSpy.mockRestore();
});

it('should not log anything if no warning header was passed', async () => {
const mock = getAPIMock().get('/api/v1/some-warning').reply(200, undefined, {
Warning: '',
});

await fetch(`${config.get('host')}/api/v1/some-warning`);

// eslint-disable-next-line no-console
expect(console.warn).toHaveBeenCalledTimes(0);
expect(getWarningCommandOutput()).toBe('');

mock.done();
});

it('should surface a single warning header', async () => {
const mock = getAPIMock().get('/api/v1/some-warning').reply(200, undefined, {
Warning: '199 - "some error"',
});

await fetch(`${config.get('host')}/api/v1/some-warning`);

// eslint-disable-next-line no-console
expect(console.warn).toHaveBeenCalledTimes(1);
expect(getWarningCommandOutput()).toBe('⚠️ ReadMe API Warning: some error');

mock.done();
});

it('should surface multiple warning headers', async () => {
const mock = getAPIMock().get('/api/v1/some-warning').reply(200, undefined, {
Warning: '199 - "some error" 199 - "another error"',
});

await fetch(`${config.get('host')}/api/v1/some-warning`);

// eslint-disable-next-line no-console
expect(console.warn).toHaveBeenCalledTimes(2);
expect(getWarningCommandOutput()).toBe(
'⚠️ ReadMe API Warning: some error\n\n⚠️ ReadMe API Warning: another error'
);

mock.done();
});

it('should surface header content even if parsing fails', async () => {
const mock = getAPIMock().get('/api/v1/some-warning').reply(200, undefined, {
Warning: 'some garbage error',
});

await fetch(`${config.get('host')}/api/v1/some-warning`);

// eslint-disable-next-line no-console
expect(console.warn).toHaveBeenCalledTimes(1);
expect(getWarningCommandOutput()).toBe('⚠️ ReadMe API Warning: some garbage error');

mock.done();
});
});

describe('proxies', () => {
afterEach(() => {
delete process.env.https_proxy;
Expand Down
64 changes: 63 additions & 1 deletion src/lib/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import pkg from '../../package.json';

import APIError from './apiError';
import { isGHA } from './isCI';
import { debug } from './logger';
import { debug, warn } from './logger';

const SUCCESS_NO_CONTENT = 204;

Expand All @@ -22,6 +22,58 @@ function getProxy() {
return '';
}

/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning}
* @see {@link https://www.rfc-editor.org/rfc/rfc7234#section-5.5}
* @see {@link https://github.com/marcbachmann/warning-header-parser}
*/
interface WarningHeader {
code: string;
agent: string;
message: string;
date?: string;
}

function stripQuotes(s: string) {
if (!s) return '';
return s.replace(/(^"|[",]*$)/g, '');
}

/**
* Parses Warning header into an array of warning header objects
* @param header raw `Warning` header
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning}
* @see {@link https://www.rfc-editor.org/rfc/rfc7234#section-5.5}
* @see {@link https://github.com/marcbachmann/warning-header-parser}
*/
function parseWarningHeader(header: string): WarningHeader[] {
try {
const warnings = header.split(/([0-9]{3} [a-z0-9.@\-/]*) /g);

let previous: WarningHeader;

return warnings.reduce((all, w) => {
// eslint-disable-next-line no-param-reassign
w = w.trim();
const newError = w.match(/^([0-9]{3}) (.*)/);
if (newError) {
previous = { code: newError[1], agent: newError[2], message: '' };
} else if (w) {
const errorContent = w.split(/" "/);
if (errorContent) {
previous.message = stripQuotes(errorContent[0]);
previous.date = stripQuotes(errorContent[1]);
all.push(previous);
}
}
return all;
}, []);
} catch (e) {
debug(`error parsing warning header: ${e.message}`);
return [{ code: '199', agent: '-', message: header }];
}
}

/**
* Getter function for a string to be used in the user-agent header based on the current
* environment.
Expand Down Expand Up @@ -64,6 +116,16 @@ export default function fetch(url: string, options: RequestInit = { headers: new
return nodeFetch(fullUrl, {
...options,
headers,
}).then(res => {
const warningHeader = res.headers.get('Warning');
if (warningHeader) {
debug(`received warning header: ${warningHeader}`);
const warnings = parseWarningHeader(warningHeader);
warnings.forEach(warning => {
warn(warning.message, 'ReadMe API Warning:');
});
}
return res;
});
}

Expand Down
6 changes: 4 additions & 2 deletions src/lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,14 @@ function oraOptions() {

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

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

0 comments on commit a210754

Please sign in to comment.