From f2af30cf902c02450ba67a1b493795c3dcef41a3 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 16 Jun 2020 17:19:01 -0700 Subject: [PATCH] browser(firefox): properly instrument requests intercepted by service worker (#2594) When httpChannel is intercepted by Service Worker: - it gets an internal redirect to another channel with the same id; - once serivce worker responds, the channel gets the data, but does not get any onResponse notifications. So, we update our ResponseBodyListener (the nsIRequestObserver implementation) to the new request and force onResponse from there once data is available or request finishes. --- browser_patches/firefox/BUILD_NUMBER | 2 +- .../firefox/juggler/NetworkObserver.js | 47 +++++++++++++++++-- .../firefox/juggler/content/NetworkMonitor.js | 18 ++++--- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/browser_patches/firefox/BUILD_NUMBER b/browser_patches/firefox/BUILD_NUMBER index af6316f1e0..0a75ff7619 100644 --- a/browser_patches/firefox/BUILD_NUMBER +++ b/browser_patches/firefox/BUILD_NUMBER @@ -1 +1 @@ -1109 +1110 diff --git a/browser_patches/firefox/juggler/NetworkObserver.js b/browser_patches/firefox/juggler/NetworkObserver.js index 2605f45655..6c01e462eb 100644 --- a/browser_patches/firefox/juggler/NetworkObserver.js +++ b/browser_patches/firefox/juggler/NetworkObserver.js @@ -154,6 +154,7 @@ class NetworkObserver { this._pendingAuthentication = new Set(); // pre-auth id this._postAuthChannelIdToRequestId = new Map(); // pre-auth id => post-auth id this._bodyListeners = new Map(); // channel id => ResponseBodyListener. + this._channelsReceivedOnResponse = new Set(); // channel ids that have seen onResponse. const protocolProxyService = Cc['@mozilla.org/network/protocol-proxy-service;1'].getService(); this._channelProxyFilter = { @@ -255,6 +256,13 @@ class NetworkObserver { } else if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL)) { // Regular (non-internal) redirect. this._redirectMap.set(newRequestId, oldRequestId); + } else { + // Requests intercepted by Service Worker get redirected to a different channel with the same id. + // In addition, they do not receive onResponse. We update body listener to use the new channel, + // and it will ensure onResponse before any data is available or request finishes. + const bodyListener = this._bodyListeners.get(oldHttpChannel.channelId + ''); + if (bodyListener) + bodyListener._httpChannel = newHttpChannel; } } @@ -443,9 +451,15 @@ class NetworkObserver { const id = httpChannel.channelId + ''; this._postResumeChannelIdToRequestId.delete(id); this._postAuthChannelIdToRequestId.delete(id); + this._channelsReceivedOnResponse.delete(id); } _onResponse(fromCache, httpChannel, topic) { + if (this._channelsReceivedOnResponse.has(httpChannel.channelId + '')) { + // We can come here twice because of service workers, see ResponseBodyLoader. + return; + } + this._channelsReceivedOnResponse.add(httpChannel.channelId + ''); const pageNetwork = this._pageNetworkForChannel(httpChannel); if (!pageNetwork) return; @@ -628,12 +642,24 @@ class ResponseBodyListener { this._networkObserver._bodyListeners.set(this._httpChannel.channelId + '', this); } + _ensureOnResponse() { + // For requests intercepted by Service Worker, we do not get onResponse normally, + // but we do get nsIRequestObserver notifications. + this._networkObserver._onResponse(false /* fromCache */, this._httpChannel, ''); + } + onDataAvailable(aRequest, aInputStream, aOffset, aCount) { if (this._disposed) { - this.originalListener.onDataAvailable(aRequest, aInputStream, aOffset, aCount); + try { + this.originalListener.onDataAvailable(aRequest, aInputStream, aOffset, aCount); + } catch (e) { + // Be ready to original listener exceptions. + } return; } + this._ensureOnResponse(); + const iStream = new BinaryInputStream(aInputStream); const sStream = new StorageStream(8192, aCount, null); const oStream = new BinaryOutputStream(sStream.getOutputStream(0)); @@ -643,19 +669,32 @@ class ResponseBodyListener { this._chunks.push(data); oStream.writeBytes(data, aCount); - this.originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount); + try { + this.originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount); + } catch (e) { + // Be ready to original listener exceptions. + } } onStartRequest(aRequest) { - this.originalListener.onStartRequest(aRequest); + try { + this.originalListener.onStartRequest(aRequest); + } catch (e) { + // Be ready to original listener exceptions. + } } onStopRequest(aRequest, aStatusCode) { - this.originalListener.onStopRequest(aRequest, aStatusCode); + try { + this.originalListener.onStopRequest(aRequest, aStatusCode); + } catch (e) { + // Be ready to original listener exceptions. + } if (this._disposed) return; if (aStatusCode === 0) { + this._ensureOnResponse(); const body = this._chunks.join(''); this._networkObserver._onResponseFinished(this._pageNetwork, this._httpChannel, body); } else { diff --git a/browser_patches/firefox/juggler/content/NetworkMonitor.js b/browser_patches/firefox/juggler/content/NetworkMonitor.js index 13b76cd8a5..fdb5e2899c 100644 --- a/browser_patches/firefox/juggler/content/NetworkMonitor.js +++ b/browser_patches/firefox/juggler/content/NetworkMonitor.js @@ -28,13 +28,17 @@ class NetworkMonitor { const loadContext = helper.getLoadContext(httpChannel); if (!loadContext) return; - const window = loadContext.associatedWindow; - const frame = this._frameTree.frameForDocShell(window.docShell); - if (!frame) - return; - this._requestDetails.set(httpChannel.channelId, { - frameId: frame.id(), - }); + try { + const window = loadContext.associatedWindow; + const frame = this._frameTree.frameForDocShell(window.docShell); + if (!frame) + return; + this._requestDetails.set(httpChannel.channelId, { + frameId: frame.id(), + }); + } catch (e) { + // Accessing loadContext.associatedWindow sometimes throws. + } } requestDetails(channelId) {