mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 12:04:04 +01:00
Rerun fallbacks (#260297)
This commit is contained in:
@@ -18,6 +18,10 @@ export class Log {
|
||||
this.output.trace(message);
|
||||
}
|
||||
|
||||
public debug(message: string): void {
|
||||
this.output.debug(message);
|
||||
}
|
||||
|
||||
public info(message: string): void {
|
||||
this.output.info(message);
|
||||
}
|
||||
|
||||
@@ -172,6 +172,7 @@ async function exchangeCodeForToken(
|
||||
}
|
||||
const result = await fetching(endpointUri.toString(true), {
|
||||
logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: true,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -406,6 +407,7 @@ class DeviceCodeFlow implements IFlow {
|
||||
});
|
||||
const result = await fetching(uri.toString(true), {
|
||||
logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: true,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -484,6 +486,7 @@ class DeviceCodeFlow implements IFlow {
|
||||
try {
|
||||
accessTokenResult = await fetching(refreshTokenUri.toString(true), {
|
||||
logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: true,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -579,6 +582,7 @@ class PatFlow implements IFlow {
|
||||
logger.info('Getting token scopes...');
|
||||
const result = await fetching(serverUri.toString(), {
|
||||
logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: false,
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
|
||||
@@ -179,6 +179,7 @@ export class GitHubServer implements IGitHubServer {
|
||||
// Defined here: https://docs.github.com/en/rest/apps/oauth-applications?apiVersion=2022-11-28#delete-an-app-token
|
||||
const result = await fetching(uri.toString(true), {
|
||||
logger: this._logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: false,
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
@@ -222,6 +223,7 @@ export class GitHubServer implements IGitHubServer {
|
||||
this._logger.info('Getting user info...');
|
||||
result = await fetching(this.getServerUri('/user').toString(), {
|
||||
logger: this._logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: true,
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
@@ -282,6 +284,7 @@ export class GitHubServer implements IGitHubServer {
|
||||
try {
|
||||
const result = await fetching('https://education.github.com/api/user', {
|
||||
logger: this._logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: true,
|
||||
headers: {
|
||||
Authorization: `token ${session.accessToken}`,
|
||||
@@ -324,6 +327,7 @@ export class GitHubServer implements IGitHubServer {
|
||||
if (!isSupportedTarget(this._type, this._ghesUri)) {
|
||||
const result = await fetching(this.getServerUri('/meta').toString(), {
|
||||
logger: this._logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: true,
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
|
||||
@@ -7,9 +7,11 @@ import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { workspace } from 'vscode';
|
||||
import { Log } from '../common/logger';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
export interface FetchOptions {
|
||||
logger: Log;
|
||||
retryFallbacks: boolean;
|
||||
expectJSON: boolean;
|
||||
method?: 'GET' | 'POST' | 'DELETE';
|
||||
headers?: Record<string, string>;
|
||||
@@ -64,97 +66,71 @@ _fetchers.push({
|
||||
});
|
||||
|
||||
export function createFetch(): Fetch {
|
||||
let _fetcher: Fetcher | undefined;
|
||||
let fetchers: readonly Fetcher[] = _fetchers;
|
||||
return async (url, options) => {
|
||||
if (!_fetcher) {
|
||||
let firstResponse: FetchResponse | undefined;
|
||||
let firstError: any;
|
||||
for (const fetcher of _fetchers) {
|
||||
try {
|
||||
const res = await fetcher.fetch(url, options);
|
||||
if (fetcher === _fetchers[0]) {
|
||||
firstResponse = res;
|
||||
}
|
||||
if (!res.ok) {
|
||||
options.logger.info(`fetching: ${fetcher.name} failed with status: ${res.status} ${res.statusText}`);
|
||||
continue;
|
||||
}
|
||||
if (!options.expectJSON) {
|
||||
options.logger.info(`fetching: ${fetcher.name} succeeded (not JSON)`);
|
||||
_fetcher = fetcher;
|
||||
return res;
|
||||
}
|
||||
const text = await res.text();
|
||||
if (fetcher === _fetchers[0]) {
|
||||
// Update to unconsumed response
|
||||
firstResponse = new FetchResponseImpl(
|
||||
res.status,
|
||||
res.statusText,
|
||||
res.headers,
|
||||
async () => text,
|
||||
async () => JSON.parse(text),
|
||||
);
|
||||
}
|
||||
const json = JSON.parse(text); // Verify JSON
|
||||
options.logger.info(`fetching: ${fetcher.name} succeeded (JSON)`);
|
||||
if (fetcher !== _fetchers[0]) {
|
||||
const retry = await retryFetch(_fetchers[0], url, options);
|
||||
if ('res' in retry && retry.res.ok) {
|
||||
_fetcher = _fetchers[0];
|
||||
return retry.res;
|
||||
}
|
||||
}
|
||||
_fetcher = fetcher;
|
||||
return new FetchResponseImpl(
|
||||
res.status,
|
||||
res.statusText,
|
||||
res.headers,
|
||||
async () => text,
|
||||
async () => json,
|
||||
);
|
||||
} catch (err) {
|
||||
if (fetcher === _fetchers[0]) {
|
||||
firstError = err;
|
||||
}
|
||||
options.logger.info(`fetching: ${fetcher.name} failed with error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
_fetcher = _fetchers[0]; // Do this only once
|
||||
if (firstResponse) {
|
||||
return firstResponse;
|
||||
}
|
||||
throw firstError;
|
||||
const result = await fetchWithFallbacks(fetchers, url, options, options.logger);
|
||||
if (result.updatedFetchers) {
|
||||
fetchers = result.updatedFetchers;
|
||||
}
|
||||
return _fetcher.fetch(url, options);
|
||||
return result.response;
|
||||
};
|
||||
}
|
||||
|
||||
async function retryFetch(fetcher: Fetcher, url: string, options: FetchOptions): Promise<{ res: FetchResponse } | { err: any }> {
|
||||
async function fetchWithFallbacks(availableFetchers: readonly Fetcher[], url: string, options: FetchOptions, logService: Log): Promise<{ response: FetchResponse; updatedFetchers?: Fetcher[] }> {
|
||||
if (options.retryFallbacks && availableFetchers.length > 1) {
|
||||
let firstResult: { ok: boolean; response: FetchResponse } | { ok: false; err: any } | undefined;
|
||||
for (const fetcher of availableFetchers) {
|
||||
const result = await tryFetch(fetcher, url, options, logService);
|
||||
if (fetcher === availableFetchers[0]) {
|
||||
firstResult = result;
|
||||
}
|
||||
if (!result.ok) {
|
||||
continue;
|
||||
}
|
||||
if (fetcher !== availableFetchers[0]) {
|
||||
const retry = await tryFetch(availableFetchers[0], url, options, logService);
|
||||
if (retry.ok) {
|
||||
return { response: retry.response };
|
||||
}
|
||||
logService.info(`FetcherService: using ${fetcher.name} from now on`);
|
||||
const updatedFetchers = availableFetchers.slice();
|
||||
updatedFetchers.splice(updatedFetchers.indexOf(fetcher), 1);
|
||||
updatedFetchers.unshift(fetcher);
|
||||
return { response: result.response, updatedFetchers };
|
||||
}
|
||||
return { response: result.response };
|
||||
}
|
||||
if ('response' in firstResult!) {
|
||||
return { response: firstResult.response };
|
||||
}
|
||||
throw firstResult!.err;
|
||||
}
|
||||
return { response: await availableFetchers[0].fetch(url, options) };
|
||||
}
|
||||
|
||||
async function tryFetch(fetcher: Fetcher, url: string, options: FetchOptions, logService: Log): Promise<{ ok: boolean; response: FetchResponse } | { ok: false; err: any }> {
|
||||
try {
|
||||
const res = await fetcher.fetch(url, options);
|
||||
if (!res.ok) {
|
||||
options.logger.info(`fetching: ${fetcher.name} failed with status: ${res.status} ${res.statusText}`);
|
||||
return { res };
|
||||
const response = await fetcher.fetch(url, options);
|
||||
if (!response.ok) {
|
||||
logService.info(`FetcherService: ${fetcher.name} failed with status: ${response.status} ${response.statusText}`);
|
||||
return { ok: false, response };
|
||||
}
|
||||
if (!options.expectJSON) {
|
||||
options.logger.info(`fetching: ${fetcher.name} succeeded (not JSON)`);
|
||||
return { res };
|
||||
logService.debug(`FetcherService: ${fetcher.name} succeeded (not JSON)`);
|
||||
return { ok: response.ok, response };
|
||||
}
|
||||
const text = await response.text();
|
||||
try {
|
||||
const json = JSON.parse(text); // Verify JSON
|
||||
logService.debug(`FetcherService: ${fetcher.name} succeeded (JSON)`);
|
||||
return { ok: true, response: new FetchResponseImpl(response.status, response.statusText, response.headers, async () => text, async () => json, async () => Readable.from([text])) };
|
||||
} catch (err) {
|
||||
logService.info(`FetcherService: ${fetcher.name} failed to parse JSON: ${err.message}`);
|
||||
return { ok: false, err, response: new FetchResponseImpl(response.status, response.statusText, response.headers, async () => text, async () => { throw err; }, async () => Readable.from([text])) };
|
||||
}
|
||||
const text = await res.text();
|
||||
const json = JSON.parse(text); // Verify JSON
|
||||
options.logger.info(`fetching: ${fetcher.name} succeeded (JSON)`);
|
||||
return {
|
||||
res: new FetchResponseImpl(
|
||||
res.status,
|
||||
res.statusText,
|
||||
res.headers,
|
||||
async () => text,
|
||||
async () => json,
|
||||
)
|
||||
};
|
||||
} catch (err) {
|
||||
options.logger.info(`fetching: ${fetcher.name} failed with error: ${err.message}`);
|
||||
return { err };
|
||||
logService.info(`FetcherService: ${fetcher.name} failed with error: ${err.message}`);
|
||||
return { ok: false, err };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +144,7 @@ class FetchResponseImpl implements FetchResponse {
|
||||
public readonly headers: FetchHeaders,
|
||||
public readonly text: () => Promise<string>,
|
||||
public readonly json: () => Promise<any>,
|
||||
public readonly body: () => Promise<NodeJS.ReadableStream | null>,
|
||||
) {
|
||||
this.ok = this.status >= 200 && this.status < 300;
|
||||
}
|
||||
@@ -192,6 +169,7 @@ async function nodeHTTP(url: string, options: FetchOptions): Promise<FetchRespon
|
||||
nodeFetcherResponse.headers,
|
||||
async () => nodeFetcherResponse.text(),
|
||||
async () => nodeFetcherResponse.json(),
|
||||
async () => nodeFetcherResponse.body(),
|
||||
));
|
||||
});
|
||||
req.setTimeout(60 * 1000); // time out after 60s of receiving no data
|
||||
|
||||
@@ -76,6 +76,7 @@ suite('fetching', () => {
|
||||
test('should use Electron fetch', async () => {
|
||||
const res = await createFetch()(`http://localhost:${port}/json`, {
|
||||
logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: true,
|
||||
});
|
||||
const actualAgent = res.headers.get('x-client-user-agent') || 'None';
|
||||
@@ -87,6 +88,7 @@ suite('fetching', () => {
|
||||
test('should use Electron fetch 2', async () => {
|
||||
const res = await createFetch()(`http://localhost:${port}/text`, {
|
||||
logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: false,
|
||||
});
|
||||
const actualAgent = res.headers.get('x-client-user-agent') || 'None';
|
||||
@@ -98,6 +100,7 @@ suite('fetching', () => {
|
||||
test('should fall back to Node.js fetch', async () => {
|
||||
const res = await createFetch()(`http://localhost:${port}/json?expectAgent=node`, {
|
||||
logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: true,
|
||||
});
|
||||
const actualAgent = res.headers.get('x-client-user-agent') || 'None';
|
||||
@@ -109,6 +112,7 @@ suite('fetching', () => {
|
||||
test('should fall back to Node.js fetch 2', async () => {
|
||||
const res = await createFetch()(`http://localhost:${port}/json?expectAgent=node&error=html`, {
|
||||
logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: true,
|
||||
});
|
||||
const actualAgent = res.headers.get('x-client-user-agent') || 'None';
|
||||
@@ -120,6 +124,7 @@ suite('fetching', () => {
|
||||
test('should fall back to Node.js http/s', async () => {
|
||||
const res = await createFetch()(`http://localhost:${port}/json?expectAgent=undefined`, {
|
||||
logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: true,
|
||||
});
|
||||
const actualAgent = res.headers.get('x-client-user-agent') || 'None';
|
||||
@@ -131,6 +136,7 @@ suite('fetching', () => {
|
||||
test('should fail with first error', async () => {
|
||||
const res = await createFetch()(`http://localhost:${port}/text`, {
|
||||
logger,
|
||||
retryFallbacks: true,
|
||||
expectJSON: true, // Expect JSON but server returns text
|
||||
});
|
||||
const actualAgent = res.headers.get('x-client-user-agent') || 'None';
|
||||
|
||||
Reference in New Issue
Block a user