Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: consolidate http/https fetching #21104

Merged
merged 2 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 7 additions & 20 deletions packages/playwright-core/src/server/chromium/chromium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,22 @@ import { Browser } from '../browser';
import type * as types from '../types';
import type * as channels from '@protocol/channels';
import type { HTTPRequestParams } from '../../utils/network';
import { NET_DEFAULT_TIMEOUT } from '../../utils/network';
import { fetchData } from '../../utils/network';
import { getUserAgent } from '../../utils/userAgent';
import { wrapInASCIIBox } from '../../utils/ascii';
import { debugMode, headersArrayToObject, } from '../../utils';
import { debugMode, headersArrayToObject, } from '../../utils';
import { removeFolders } from '../../utils/fileUtils';
import { RecentLogsCollector } from '../../common/debugLogger';
import type { Progress } from '../progress';
import { ProgressController } from '../progress';
import { TimeoutSettings } from '../../common/timeoutSettings';
import { helper } from '../helper';
import type { CallMetadata } from '../instrumentation';
import http from 'http';
import https from 'https';
import type http from 'http';
import { registry } from '../registry';
import { ManualPromise } from '../../utils/manualPromise';
import { validateBrowserContextOptions } from '../browserContext';
import { chromiumSwitches } from './chromiumSwitches';
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../happy-eyeballs';

const ARTIFACTS_FOLDER = path.join(os.tmpdir(), 'playwright-artifacts-');

Expand Down Expand Up @@ -338,21 +335,11 @@ async function urlToWSEndpoint(progress: Progress, endpointURL: string) {
return endpointURL;
progress.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
const httpURL = endpointURL.endsWith('/') ? `${endpointURL}json/version/` : `${endpointURL}/json/version/`;
const isHTTPS = endpointURL.startsWith('https://');
const json = await new Promise<string>((resolve, reject) => {
(isHTTPS ? https : http).get(httpURL, {
timeout: NET_DEFAULT_TIMEOUT,
agent: isHTTPS ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent,
}, resp => {
if (resp.statusCode! < 200 || resp.statusCode! >= 400) {
reject(new Error(`Unexpected status ${resp.statusCode} when connecting to ${httpURL}.\n` +
`This does not look like a DevTools server, try connecting via ws://.`));
}
let data = '';
resp.on('data', chunk => data += chunk);
resp.on('end', () => resolve(data));
}).on('error', reject);
});
const json = await fetchData({
url: httpURL,
}, async (_, resp) => new Error(`Unexpected status ${resp.statusCode} when connecting to ${httpURL}.\n` +
`This does not look like a DevTools server, try connecting via ws://.`)
);
return JSON.parse(json).webSocketDebuggerUrl;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
import { BrowserContext } from './browserContext';
import { CookieStore, domainMatches } from './cookieStore';
import { MultipartFormData } from './formData';
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from './happy-eyeballs';
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';
import type { CallMetadata } from './instrumentation';
import { SdkObject } from './instrumentation';
import type { Playwright } from './playwright';
Expand Down Expand Up @@ -69,7 +69,7 @@ export type APIRequestFinishedEvent = {
body?: Buffer;
};

export type SendRequestOptions = https.RequestOptions & {
type SendRequestOptions = https.RequestOptions & {
maxRedirects: number,
deadline: number,
__testHookLookup?: (hostname: string) => LookupAddress[]
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { WebSocket } from '../utilsBundle';
import type { ClientRequest, IncomingMessage } from 'http';
import type { Progress } from './progress';
import { makeWaitForNextTask } from '../utils';
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from './happy-eyeballs';
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';

export type ProtocolRequest = {
id: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import * as http from 'http';
import * as https from 'https';
import * as net from 'net';
import * as tls from 'tls';
import { ManualPromise } from '../utils/manualPromise';
import type { SendRequestOptions } from './fetch';
import { ManualPromise } from './manualPromise';

// Implementation(partial) of Happy Eyeballs 2 algorithm described in
// https://www.rfc-editor.org/rfc/rfc8305
Expand Down Expand Up @@ -50,7 +49,7 @@ export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent();
export const httpHappyEyeballsAgent = new HttpHappyEyeballsAgent();

async function createConnectionAsync(options: http.ClientRequestArgs, oncreate: ((err: Error | null, socket?: net.Socket) => void) | undefined, useTLS: boolean) {
const lookup = (options as SendRequestOptions).__testHookLookup || lookupAddresses;
const lookup = (options as any).__testHookLookup || lookupAddresses;
const hostname = clientRequestArgsToHostName(options);
const addresses = await lookup(hostname);
const sockets = new Set<net.Socket>();
Expand Down
14 changes: 11 additions & 3 deletions packages/playwright-core/src/utils/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import * as URL from 'url';
import type { URLMatch } from '../common/types';
import { isString, isRegExp } from './rtti';
import { globToRegex } from './glob';
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from './happy-eyeballs';

export async function createSocket(host: string, port: number): Promise<net.Socket> {
return new Promise((resolve, reject) => {
Expand All @@ -39,15 +40,22 @@ export type HTTPRequestParams = {
headers?: http.OutgoingHttpHeaders,
data?: string | Buffer,
timeout?: number,
rejectUnauthorized?: boolean,
};

export const NET_DEFAULT_TIMEOUT = 30_000;

export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.IncomingMessage) => void, onError: (error: Error) => void) {
const parsedUrl = URL.parse(params.url);
let options: https.RequestOptions = { ...parsedUrl };
options.method = params.method || 'GET';
options.headers = params.headers;
let options: https.RequestOptions = {
...parsedUrl,
agent: parsedUrl.protocol === 'https:' ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent,
method: params.method || 'GET',
headers: params.headers,
};
if (params.rejectUnauthorized !== undefined)
options.rejectUnauthorized = params.rejectUnauthorized;

const timeout = params.timeout ?? NET_DEFAULT_TIMEOUT;

const proxyURL = getProxyForUrl(params.url);
Expand Down
18 changes: 7 additions & 11 deletions packages/playwright-test/src/plugins/webServerPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import http from 'http';
import https from 'https';
import path from 'path';
import net from 'net';

import { debug } from 'playwright-core/lib/utilsBundle';
import { raceAgainstTimeout, launchProcess } from 'playwright-core/lib/utils';
import { raceAgainstTimeout, launchProcess, httpRequest } from 'playwright-core/lib/utils';

import type { FullConfig, Reporter } from '../../types/testReporter';
import type { TestRunnerPlugin } from '.';
Expand Down Expand Up @@ -159,20 +157,18 @@ async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onStdErr: Re
}

async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onStdErr: Reporter['onStdErr']): Promise<number> {
const commonRequestOptions = { headers: { Accept: '*/*' } };
const isHttps = url.protocol === 'https:';
const requestOptions = isHttps ? {
...commonRequestOptions,
rejectUnauthorized: !ignoreHTTPSErrors,
} : commonRequestOptions;
return new Promise(resolve => {
debugWebServer(`HTTP GET: ${url}`);
(isHttps ? https : http).get(url, requestOptions, res => {
httpRequest({
url: url.toString(),
headers: { Accept: '*/*' },
rejectUnauthorized: !ignoreHTTPSErrors
}, res => {
res.resume();
const statusCode = res.statusCode ?? 0;
debugWebServer(`HTTP Status: ${statusCode}`);
resolve(statusCode);
}).on('error', error => {
}, error => {
if ((error as NodeJS.ErrnoException).code === 'DEPTH_ZERO_SELF_SIGNED_CERT')
onStdErr?.(`[WebServer] Self-signed certificate detected. Try adding ignoreHTTPSErrors: true to config.webServer.`);
debugWebServer(`Error while checking if ${url} is available: ${error.message}`);
Expand Down
24 changes: 24 additions & 0 deletions tests/playwright-test/web-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,3 +608,27 @@ test('should treat 3XX as available server', async ({ runInlineTest }, { workerI
expect(result.output).toContain('[WebServer] listening');
expect(result.output).toContain('[WebServer] error from server');
});

test('should check ipv4 and ipv6 with happy eyeballs when URL is passed', async ({ runInlineTest }, { workerIndex }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/20784' });
const port = workerIndex * 2 + 10500;
const result = await runInlineTest({
'test.spec.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({}) => {});
`,
'playwright.config.ts': `
module.exports = {
webServer: {
command: 'node -e "require(\\'http\\').createServer((req, res) => res.end()).listen(${port}, \\'127.0.0.1\\')"',
url: 'http://localhost:${port}/',
}
};
`,
}, {}, { DEBUG: 'pw:webserver' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(result.output).toContain('Process started');
expect(result.output).toContain(`HTTP GET: http://localhost:${port}/`);
expect(result.output).toContain('WebServer available');
});