playwright/browser_patches/firefox/juggler/protocol/NetworkHandler.js
Dmitry Gozman 5364c6acbc
browser(firefox): fix automatic http->https redirect (#3812)
browser(firefox): fix automatic http->https redirect

Sometimes, Firefox does an automatic http->https redirect without hitting
the network (e.g. for http://wikipedia.org). In this case, the http request
is very strange:
- it does not actually hit the network;
- it is never intercepted;
- we cannot access its response because there was no actual response.

So, we had a bug where:
- redirects inherited the original request's listener;
- that listener was throwing an error.
This lead to the error in the listeners onDataAvailable call chain,
and original listener that renders the response was never called,
resulting in an empty page.

This change:
- ignores the original request that did not hit the network;
- does not inherit the listener;
- adds try/catch around problematic calls.
2020-09-09 09:16:03 -07:00

160 lines
5.7 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
const helper = new Helper();
class NetworkHandler {
constructor(target, session, contentChannel) {
this._session = session;
this._contentPage = contentChannel.connect(session.sessionId() + 'page');
this._httpActivity = new Map();
this._enabled = false;
this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target);
this._requestInterception = false;
this._eventListeners = [];
this._pendingRequstWillBeSentEvents = new Set();
}
async enable() {
if (this._enabled)
return;
this._enabled = true;
this._eventListeners = [
helper.on(this._pageNetwork, PageNetwork.Events.Request, this._onRequest.bind(this)),
helper.on(this._pageNetwork, PageNetwork.Events.Response, this._onResponse.bind(this)),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._onRequestFinished.bind(this)),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._onRequestFailed.bind(this)),
this._pageNetwork.addSession(),
];
}
async getResponseBody({requestId}) {
return this._pageNetwork.getResponseBody(requestId);
}
async setExtraHTTPHeaders({headers}) {
this._pageNetwork.setExtraHTTPHeaders(headers);
}
async setRequestInterception({enabled}) {
if (enabled)
this._pageNetwork.enableRequestInterception();
else
this._pageNetwork.disableRequestInterception();
// Right after we enable/disable request interception we need to await all pending
// requestWillBeSent events before successfully returning from the method.
await Promise.all(Array.from(this._pendingRequstWillBeSentEvents));
}
async resumeInterceptedRequest({requestId, method, headers, postData}) {
this._pageNetwork.resumeInterceptedRequest(requestId, method, headers, postData);
}
async abortInterceptedRequest({requestId, errorCode}) {
this._pageNetwork.abortInterceptedRequest(requestId, errorCode);
}
async fulfillInterceptedRequest({requestId, status, statusText, headers, base64body}) {
this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body);
}
dispose() {
this._contentPage.dispose();
helper.removeListeners(this._eventListeners);
}
_ensureHTTPActivity(requestId) {
let activity = this._httpActivity.get(requestId);
if (!activity) {
activity = {
_id: requestId,
_lastSentEvent: null,
request: null,
response: null,
complete: null,
failed: null,
};
this._httpActivity.set(requestId, activity);
}
return activity;
}
_reportHTTPAcitivityEvents(activity) {
// State machine - sending network events.
if (!activity._lastSentEvent && activity.request) {
this._session.emitEvent('Network.requestWillBeSent', activity.request);
activity._lastSentEvent = 'requestWillBeSent';
}
if (activity._lastSentEvent === 'requestWillBeSent' && activity.response) {
this._session.emitEvent('Network.responseReceived', activity.response);
activity._lastSentEvent = 'responseReceived';
}
if (activity._lastSentEvent === 'responseReceived' && activity.complete) {
this._session.emitEvent('Network.requestFinished', activity.complete);
activity._lastSentEvent = 'requestFinished';
}
if (activity._lastSentEvent && activity.failed) {
this._session.emitEvent('Network.requestFailed', activity.failed);
activity._lastSentEvent = 'requestFailed';
}
// Clean up if request lifecycle is over.
if (activity._lastSentEvent === 'requestFinished' || activity._lastSentEvent === 'requestFailed')
this._httpActivity.delete(activity._id);
}
async _onRequest(eventDetails, channelKey) {
let pendingRequestCallback;
let pendingRequestPromise = new Promise(x => pendingRequestCallback = x);
this._pendingRequstWillBeSentEvents.add(pendingRequestPromise);
let details = null;
try {
details = await this._contentPage.send('requestDetails', {channelKey});
} catch (e) {
pendingRequestCallback();
this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise);
return;
}
const frameId = details ? details.frameId : undefined;
const activity = this._ensureHTTPActivity(eventDetails.requestId);
activity.request = {
frameId,
...eventDetails,
};
this._reportHTTPAcitivityEvents(activity);
pendingRequestCallback();
this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise);
}
async _onResponse(eventDetails) {
const activity = this._ensureHTTPActivity(eventDetails.requestId);
activity.response = eventDetails;
this._reportHTTPAcitivityEvents(activity);
}
async _onRequestFinished(eventDetails) {
const activity = this._ensureHTTPActivity(eventDetails.requestId);
activity.complete = eventDetails;
this._reportHTTPAcitivityEvents(activity);
}
async _onRequestFailed(eventDetails) {
const activity = this._ensureHTTPActivity(eventDetails.requestId);
activity.failed = eventDetails;
this._reportHTTPAcitivityEvents(activity);
}
}
var EXPORTED_SYMBOLS = ['NetworkHandler'];
this.NetworkHandler = NetworkHandler;