Skip to content

Commit

Permalink
feat: use the browser networks stack as fallback when direct load fails
Browse files Browse the repository at this point in the history
Chrome recently(ish) added a `Network.loadNetworkResource` request which
allows us to ask the browser to load a resource, and get the response.
Instead of our previous hacky method of asking and then applying cookies
manually, which only worked for some scenarios, use this method as a
fallback if manually requesting the resource fails.

Do some rearranging since previously the SourceMapFactory was global
which prevented it from using session-specific data.

Fixes #1766
Fixes #425 (3 years later!)
  • Loading branch information
connor4312 committed Aug 2, 2023
1 parent 670bb82 commit 7bba7e2
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 125 deletions.
72 changes: 0 additions & 72 deletions src/adapter/resourceProvider/resourceProviderState.ts

This file was deleted.

75 changes: 65 additions & 10 deletions src/adapter/resourceProvider/statefulResourceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,24 @@ import { Response } from '.';
import Cdp from '../../cdp/api';
import { ICdpApi } from '../../cdp/connection';
import { DisposableList, IDisposable } from '../../common/disposable';
import { ILogger, LogTag } from '../../common/logging';
import { FS, FsPromises } from '../../ioc-extras';
import { ITarget } from '../../targets/targets';
import { BasicResourceProvider } from './basicResourceProvider';
import { IRequestOptionsProvider } from './requestOptionsProvider';
import { ResourceProviderState } from './resourceProviderState';

@injectable()
export class StatefulResourceProvider extends BasicResourceProvider implements IDisposable {
private readonly disposables = new DisposableList();

constructor(
@inject(FS) fs: FsPromises,
@inject(ResourceProviderState) private readonly state: ResourceProviderState,
@optional() @inject(ICdpApi) cdp?: Cdp.Api,
@inject(ILogger) private readonly logger: ILogger,
@optional() @inject(ITarget) private readonly target?: ITarget,
@optional() @inject(ICdpApi) private readonly cdp?: Cdp.Api,
@optional() @inject(IRequestOptionsProvider) options?: IRequestOptionsProvider,
) {
super(fs, options);
if (cdp) {
this.disposables.push(this.state.attach(cdp));
}
}

/**
Expand All @@ -44,12 +43,68 @@ export class StatefulResourceProvider extends BasicResourceProvider implements I
): Promise<Response<string>> {
const res = await super.fetchHttp(url, cancellationToken, headers);
if (!res.ok) {
const updated = await this.state.apply(url, headers);
if (updated !== headers) {
return await super.fetchHttp(url, cancellationToken, updated);
}
this.logger.info(LogTag.Runtime, 'Network load failed, falling back to CDP', { url, res });
return this.fetchOverBrowserNetwork(url, res);
}

return res;
}

private async fetchOverBrowserNetwork(
url: string,
original: Response<string>,
): Promise<Response<string>> {
if (!this.cdp) {
return original;
}

const res = await this.cdp.Network.loadNetworkResource({
// Browser targets use the frame ID as their target ID.
frameId: this.target?.targetInfo.targetId,
url,
options: {
includeCredentials: true,
disableCache: true,
},
});

if (!res) {
return original;
}

if (
!res.resource.success ||
!res.resource.httpStatusCode ||
res.resource.httpStatusCode >= 400 ||
!res.resource.stream
) {
return original;
}

const result: string[] = [];
let offset = 0;
while (true) {
const chunkRes = await this.cdp.IO.read({ handle: res.resource.stream, offset });
if (!chunkRes) {
this.logger.info(LogTag.Runtime, 'Stream error encountered in middle, falling back', {
url,
});
return original;
}

const chunk = chunkRes.base64Encoded ? Buffer.from(chunkRes.data, 'base64') : chunkRes.data;
offset += chunk.length;
result.push(chunk.toString());
if (chunkRes.eof) {
break;
}
}

return {
ok: true,
body: result.join(''),
statusCode: res.resource.httpStatusCode,
url,
};
}
}
41 changes: 22 additions & 19 deletions src/common/sourceMaps/sourceMapFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { expect } from 'chai';
import dataUriToBuffer from 'data-uri-to-buffer';
import { stub } from 'sinon';
import { RawIndexMap, RawSourceMap } from 'source-map';
import { IResourceProvider } from '../../adapter/resourceProvider';
import { stubbedDapApi, StubDapApi } from '../../dap/stubbedApi';
import { Logger } from '../logging/logger';
import { RawIndexMapUnresolved, SourceMapFactory } from './sourceMapFactory';
import { RawIndexMapUnresolved, RootSourceMapFactory } from './sourceMapFactory';

const toDataUri = (obj: unknown) =>
'data:application/json;base64,' + Buffer.from(JSON.stringify(obj)).toString('base64');
Expand Down Expand Up @@ -43,11 +44,26 @@ const unresolvedIndexedSourceMap: RawIndexMapUnresolved = {

describe('SourceMapFactory', () => {
let stubDap: StubDapApi;
let factory: SourceMapFactory;
let factory: RootSourceMapFactory;
let resourceProvider: IResourceProvider;

beforeEach(() => {
stubDap = stubbedDapApi();
factory = new SourceMapFactory(
resourceProvider = {
fetch(url) {
return Promise.resolve({
ok: true,
body: dataUriToBuffer(url).toString('utf8'),
url: url,
statusCode: 500,
});
},
fetchJson<T>() {
return Promise.resolve({ ok: true, body: {} as T, url: '', statusCode: 200 });
},
};

factory = new RootSourceMapFactory(
{
rebaseRemoteToLocal() {
return '/tmp/local';
Expand All @@ -65,26 +81,13 @@ describe('SourceMapFactory', () => {
return undefined;
},
},
{
fetch(url) {
return Promise.resolve({
ok: true,
body: dataUriToBuffer(url).toString('utf8'),
url: url,
statusCode: 500,
});
},
fetchJson<T>() {
return Promise.resolve({ ok: true, body: {} as T, url: '', statusCode: 200 });
},
},
stubDap.actual,
Logger.null,
);
});

it('loads indexed source-maps', async () => {
const map = await factory.load({
const map = await factory.load(resourceProvider, {
sourceMapUrl: toDataUri(indexedSourceMap),
compiledPath: '/tmp/local/one.js',
});
Expand All @@ -93,7 +96,7 @@ describe('SourceMapFactory', () => {
});

it('loads indexed source-maps with unresolved children', async () => {
const map = await factory.load({
const map = await factory.load(resourceProvider, {
sourceMapUrl: toDataUri(unresolvedIndexedSourceMap),
compiledPath: '/tmp/local/one.js',
});
Expand All @@ -103,7 +106,7 @@ describe('SourceMapFactory', () => {

it('warns without failure if a single nested child fails', async () => {
const warn = stub(Logger.null, 'warn');
const map = await factory.load({
const map = await factory.load(resourceProvider, {
sourceMapUrl: toDataUri({
...unresolvedIndexedSourceMap,
sections: [
Expand Down
Loading

0 comments on commit 7bba7e2

Please sign in to comment.