Skip to content

Commit 7edd3a4

Browse files
authored
Merge pull request #258 from tediousjs/fix/cu-download-403
fix: fetch cumulative update pages with native fetch to avoid 403s
2 parents 819cf75 + 25dfb56 commit 7edd3a4

6 files changed

Lines changed: 43 additions & 28 deletions

File tree

‎lib/main/index.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎lib/main/index.js.map‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package-lock.json‎

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
"@actions/core": "^3.0.1",
5555
"@actions/exec": "^3.0.0",
5656
"@actions/glob": "^0.7.0",
57-
"@actions/http-client": "^4.0.1",
5857
"@actions/io": "^3.0.2",
5958
"@actions/tool-cache": "^4.0.0"
6059
}

‎src/utils.ts‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { readdir } from 'node:fs/promises';
33
import * as core from '@actions/core';
44
import * as exec from '@actions/exec';
55
import * as glob from '@actions/glob';
6-
import { HttpClient } from '@actions/http-client';
76
import * as io from '@actions/io';
87
import * as tc from '@actions/tool-cache';
98
import { generateFileHash } from './crypto.ts';
@@ -240,10 +239,16 @@ export async function downloadUpdateInstaller(config: VersionConfig): Promise<st
240239
// resolve download url
241240
let downloadLink: string | null = null;
242241
if (!config.updateUrl.endsWith('.exe')) {
243-
const client = new HttpClient();
244-
const res = await client.get(config.updateUrl);
245-
if (res.message.statusCode && res.message.statusCode >= 200 && res.message.statusCode < 300) {
246-
const body = await res.readBody();
242+
// Use native fetch - the download page rejects requests made through
243+
// @actions/http-client >= 3.0.1 with a 403, regardless of user agent.
244+
const res = await fetch(config.updateUrl, {
245+
headers: {
246+
'accept': 'text/html,application/xhtml+xml',
247+
'user-agent': 'tediousjs/setup-sqlserver (+https://github.com/tediousjs/setup-sqlserver)',
248+
},
249+
});
250+
if (res.ok) {
251+
const body = await res.text();
247252
const [, link] = body.match(/\s+href\s*=\s*["'](https:\/\/download\.microsoft\.com\/.*\.exe)['"]/) ?? [];
248253
if (link) {
249254
downloadLink = link;
@@ -254,7 +259,7 @@ export async function downloadUpdateInstaller(config: VersionConfig): Promise<st
254259
}
255260
if (!downloadLink) {
256261
core.warning('Unable to download cumulative updates');
257-
core.info(`Response code: ${res.message.statusCode}`);
262+
core.info(`Response code: ${res.status}`);
258263
return '';
259264
}
260265
}

‎test/utils.ts‎

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,13 @@ const io = { mv: mock.fn(async () => {}) };
2727
const globCreate = mock.fn(async () => ({ glob: async () => [] as string[] }));
2828
const glob = { create: globCreate };
2929

30-
const httpGet = mock.fn(async () => httpResponse);
31-
const httpResponse = {
32-
message: { statusCode: 200 },
33-
readBody: mock.fn(async () => ''),
30+
const fetchResponse = {
31+
ok: true,
32+
status: 200,
33+
text: mock.fn(async () => ''),
3434
};
35-
class HttpClient {
36-
get = httpGet;
37-
}
38-
const http = { HttpClient };
35+
const fetchMock = mock.fn(async () => fetchResponse);
36+
globalThis.fetch = fetchMock as unknown as typeof fetch;
3937

4038
const readdir = mock.fn(async () => [] as string[]);
4139
const generateFileHash = mock.fn(async () => randomBytes(32));
@@ -45,7 +43,6 @@ mock.module('@actions/exec', { namedExports: exec });
4543
mock.module('@actions/tool-cache', { namedExports: tc });
4644
mock.module('@actions/io', { namedExports: io });
4745
mock.module('@actions/glob', { namedExports: glob });
48-
mock.module('@actions/http-client', { namedExports: http });
4946
mock.module('node:fs/promises', { namedExports: { readdir } });
5047
mock.module('../src/crypto.ts', { namedExports: { generateFileHash } });
5148

@@ -58,7 +55,7 @@ function resetAll() {
5855
core.startGroup, core.endGroup, core.isDebug, core.platform.getDetails,
5956
exec.exec,
6057
tc.downloadTool, tc.cacheFile, tc.cacheDir,
61-
io.mv, globCreate, httpGet, httpResponse.readBody,
58+
io.mv, globCreate, fetchMock, fetchResponse.text,
6259
readdir, generateFileHash,
6360
];
6461
for (const fn of fns) fn.mock.resetCalls();
@@ -72,9 +69,10 @@ function resetAll() {
7269
tc.cacheDir.mock.mockImplementation(async () => `C:/tools/${randomUUID()}`);
7370
io.mv.mock.mockImplementation(async () => {});
7471
globCreate.mock.mockImplementation(async () => ({ glob: async () => [] }));
75-
httpResponse.message = { statusCode: 200 };
76-
httpResponse.readBody.mock.mockImplementation(async () => '');
77-
httpGet.mock.mockImplementation(async () => httpResponse);
72+
fetchResponse.ok = true;
73+
fetchResponse.status = 200;
74+
fetchResponse.text.mock.mockImplementation(async () => '');
75+
fetchMock.mock.mockImplementation(async () => fetchResponse);
7876
readdir.mock.mockImplementation(async () => []);
7977
generateFileHash.mock.mockImplementation(async () => randomBytes(32));
8078
}
@@ -312,8 +310,9 @@ describe('utils', () => {
312310
});
313311
describe('.downloadUpdateInstaller()', () => {
314312
beforeEach(() => {
315-
httpResponse.message = { statusCode: 200 };
316-
httpResponse.readBody.mock.mockImplementation(async () => '<a href="https://download.microsoft.com/update.exe">');
313+
fetchResponse.ok = true;
314+
fetchResponse.status = 200;
315+
fetchResponse.text.mock.mockImplementation(async () => '<a href="https://download.microsoft.com/update.exe">');
317316
});
318317
it('returns a path to an exe', async () => {
319318
const res = await utils.downloadUpdateInstaller({
@@ -349,17 +348,30 @@ describe('utils', () => {
349348
updateUrl: 'https://example.com/sqlupdate.exe',
350349
});
351350
assert.match(res, /^C:\/tools\/[a-f0-9-]*\/sqlupdate\.exe$/);
352-
assert.equal(httpGet.mock.callCount(), 0);
351+
assert.equal(fetchMock.mock.callCount(), 0);
353352
});
354353
it('returns empty string if URL is not resolved', async () => {
355-
httpResponse.readBody.mock.mockImplementation(async () => '<a href="https://example.com/update.exe">');
354+
fetchResponse.text.mock.mockImplementation(async () => '<a href="https://example.com/update.exe">');
356355
const res = await utils.downloadUpdateInstaller({
357356
exeUrl: 'https://example.com/installer.exe',
358357
version: '2022',
359358
updateUrl: 'https://example.com/sqlupdate.html',
360359
});
361360
assert.equal(res, '');
362-
assert.equal(httpGet.mock.callCount(), 1);
361+
assert.equal(fetchMock.mock.callCount(), 1);
362+
});
363+
it('returns empty string if the update page request is rejected', async () => {
364+
fetchResponse.ok = false;
365+
fetchResponse.status = 403;
366+
const res = await utils.downloadUpdateInstaller({
367+
exeUrl: 'https://example.com/installer.exe',
368+
version: '2022',
369+
updateUrl: 'https://example.com/sqlupdate.html',
370+
});
371+
assert.equal(res, '');
372+
const calls = core.info.mock.calls.filter((c) => String(c.arguments[0]).startsWith('Response code'));
373+
assert.equal(calls.length, 1);
374+
assert.equal(String(calls[0].arguments[0]), 'Response code: 403');
363375
});
364376
});
365377
describe('.gatherSummaryFiles()', () => {

0 commit comments

Comments
 (0)