Skip to content

Commit 6df5063

Browse files
authored
feat: add support for HTTP redirect (#341)
Corepack will now follow the `Location` header when receiving a 30x HTTP status code. This should allow Corepack to work with proxies that redirect to a different URL.
1 parent e8ae337 commit 6df5063

File tree

2 files changed

+115
-12
lines changed

2 files changed

+115
-12
lines changed

sources/httpUtils.ts

+20-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {UsageError} from 'clipanion';
2-
import {RequestOptions} from 'https';
3-
import {IncomingMessage} from 'http';
1+
import {UsageError} from 'clipanion';
2+
import {RequestOptions} from 'https';
3+
import {IncomingMessage, ClientRequest} from 'http';
44

55
export async function fetchUrlStream(url: string, options: RequestOptions = {}) {
66
if (process.env.COREPACK_ENABLE_NETWORK === `0`)
@@ -13,17 +13,25 @@ export async function fetchUrlStream(url: string, options: RequestOptions = {})
1313
const proxyAgent = new ProxyAgent();
1414

1515
return new Promise<IncomingMessage>((resolve, reject) => {
16-
const request = https.get(url, {...options, agent: proxyAgent}, response => {
17-
const statusCode = response.statusCode;
18-
if (statusCode != null && statusCode >= 200 && statusCode < 300)
19-
return resolve(response);
16+
const createRequest = (url: string) => {
17+
const request: ClientRequest = https.get(url, {...options, agent: proxyAgent}, response => {
18+
const statusCode = response.statusCode;
2019

21-
return reject(new Error(`Server answered with HTTP ${statusCode} when performing the request to ${url}; for troubleshooting help, see https://github.com/nodejs/corepack#troubleshooting`));
22-
});
20+
if ([301, 302, 307, 308].includes(statusCode as number) && response.headers.location)
21+
return createRequest(response.headers.location as string);
2322

24-
request.on(`error`, err => {
25-
reject(new Error(`Error when performing the request to ${url}; for troubleshooting help, see https://github.com/nodejs/corepack#troubleshooting`));
26-
});
23+
if (statusCode != null && statusCode >= 200 && statusCode < 300)
24+
return resolve(response);
25+
26+
return reject(new Error(`Server answered with HTTP ${statusCode} when performing the request to ${url}; for troubleshooting help, see https://github.com/nodejs/corepack#troubleshooting`));
27+
});
28+
29+
request.on(`error`, err => {
30+
reject(new Error(`Error when performing the request to ${url}; for troubleshooting help, see https://github.com/nodejs/corepack#troubleshooting`));
31+
});
32+
};
33+
34+
createRequest(url);
2735
});
2836
}
2937

tests/httpUtils.test.ts

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {jest, describe, beforeEach, beforeAll, it, expect} from '@jest/globals';
2+
3+
import {fetchUrlStream} from '../sources/httpUtils';
4+
5+
6+
describe(`http utils fetchUrlStream`, () => {
7+
const getUrl = (statusCode: number | string, redirectCode?: number | string) =>
8+
`https://registry.example.org/answered/${statusCode}${redirectCode ? `?redirectCode=${redirectCode}` : ``}`;
9+
10+
const httpsGetFn = jest.fn((url: string, _, callback: (response: any) => void) => {
11+
const parsedURL = new URL(url);
12+
const statusCode = parsedURL.pathname.slice(parsedURL.pathname.lastIndexOf(`/`) + 1);
13+
const response = {url, statusCode: +statusCode};
14+
const errorCallbacks: Array<(err: string) => void> = [];
15+
16+
if ([301, 302, 307, 308].includes(+statusCode)) {
17+
const redirectCode = parsedURL.searchParams.get(`redirectCode`)!;
18+
// mock response.headers.location
19+
if (redirectCode) {
20+
Reflect.set(response, `headers`, {location: getUrl(redirectCode)});
21+
}
22+
}
23+
24+
// handle request.on('error', err => ...)
25+
if (statusCode === `error`)
26+
process.nextTick(() => errorCallbacks.forEach(cb => cb(`Test internal error`)));
27+
else
28+
callback(response);
29+
30+
return {
31+
on: (type: string, callback: (err: string) => void) => {
32+
if (type === `error`) {
33+
errorCallbacks.push(callback);
34+
}
35+
},
36+
};
37+
});
38+
39+
beforeAll(() => {
40+
jest.doMock(`https`, () => ({
41+
get: httpsGetFn,
42+
Agent: class Agent {},
43+
}));
44+
});
45+
46+
beforeEach(() => {
47+
httpsGetFn.mockClear();
48+
});
49+
50+
it(`correct response answered statusCode should be >= 200 and < 300`, async () => {
51+
await expect(fetchUrlStream(getUrl(200))).resolves.toMatchObject({
52+
statusCode: 200,
53+
});
54+
55+
await expect(fetchUrlStream(getUrl(299))).resolves.toMatchObject({
56+
statusCode: 299,
57+
});
58+
59+
expect(httpsGetFn).toHaveBeenCalledTimes(2);
60+
});
61+
62+
it(`bad response`, async () => {
63+
await expect(fetchUrlStream(getUrl(300))).rejects.toThrowError();
64+
await expect(fetchUrlStream(getUrl(199))).rejects.toThrowError();
65+
});
66+
67+
it(`redirection with correct response`, async () => {
68+
await expect(fetchUrlStream(getUrl(301, 200))).resolves.toMatchObject({
69+
statusCode: 200,
70+
});
71+
72+
expect(httpsGetFn).toHaveBeenCalledTimes(2);
73+
74+
await expect(fetchUrlStream(getUrl(308, 299))).resolves.toMatchObject({
75+
statusCode: 299,
76+
});
77+
78+
expect(httpsGetFn).toHaveBeenCalledTimes(4);
79+
});
80+
81+
it(`redirection with bad response`, async () => {
82+
await expect(fetchUrlStream(getUrl(301, 300))).rejects.toThrowError();
83+
await expect(fetchUrlStream(getUrl(308, 199))).rejects.toThrowError();
84+
await expect(fetchUrlStream(getUrl(301, 302))).rejects.toThrowError();
85+
await expect(fetchUrlStream(getUrl(307))).rejects.toThrowError();
86+
});
87+
88+
it(`rejects with error`, async () => {
89+
await expect(fetchUrlStream(getUrl(`error`))).rejects.toThrowError();
90+
});
91+
92+
it(`rejects when redirection with error`, async () => {
93+
await expect(fetchUrlStream(getUrl(307, `error`))).rejects.toThrowError();
94+
});
95+
});

0 commit comments

Comments
 (0)