Bug 1618543 - Let `fetch()` use "fetch" preloads, r=baku
authorHonza Bambas <honzab.moz@firemni.cz>
Mon, 18 May 2020 12:18:14 +0000
changeset 530585 4068b1f7a903cb0de7316cf2947dce6798587711
parent 530584 17b0c4bfc37d533a90af30839b85058615015c30
child 530586 f9edbff92af587d0a2978f8392e30d209200e349
push id37428
push usernbeleuzu@mozilla.com
push dateMon, 18 May 2020 21:48:24 +0000
treeherdermozilla-central@a3941f42e662 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku
bugs1618543
milestone78.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1618543 - Let `fetch()` use "fetch" preloads, r=baku Differential Revision: https://phabricator.services.mozilla.com/D74899
dom/fetch/FetchDriver.cpp
dom/fetch/FetchDriver.h
uriloader/preload/PreloaderBase.cpp
uriloader/preload/PreloaderBase.h
--- a/dom/fetch/FetchDriver.cpp
+++ b/dom/fetch/FetchDriver.cpp
@@ -351,16 +351,94 @@ FetchDriver::FetchDriver(SafeRefPtr<Inte
 FetchDriver::~FetchDriver() {
   AssertIsOnMainThread();
 
   // We assert this since even on failures, we should call
   // FailWithNetworkError().
   MOZ_ASSERT(mResponseAvailableCalled);
 }
 
+already_AddRefed<PreloaderBase> FetchDriver::FindPreload(nsIURI* aURI) {
+  // Decide if we allow reuse of an existing <link rel=preload as=fetch>
+  // response for this request.  First examine this fetch requets itself if it
+  // is 'pure' enough to use the response and then try to find a preload.
+
+  if (!mDocument) {
+    // Preloads are mapped on the document, no document, no preload.
+    return nullptr;
+  }
+  CORSMode cors;
+  switch (mRequest->Mode()) {
+    case RequestMode::No_cors:
+      cors = CORSMode::CORS_NONE;
+      break;
+    case RequestMode::Cors:
+      cors = mRequest->GetCredentialsMode() == RequestCredentials::Include
+                 ? CORSMode::CORS_USE_CREDENTIALS
+                 : CORSMode::CORS_ANONYMOUS;
+      break;
+    default:
+      // Can't be satisfied by a preload because preload cannot define any of
+      // remaining modes.
+      return nullptr;
+  }
+  if (!mRequest->Headers()->HasOnlySimpleHeaders()) {
+    // Preload can't set any headers.
+    return nullptr;
+  }
+  if (!mRequest->GetIntegrity().IsEmpty()) {
+    // There is currently no support for SRI checking in the fetch preloader.
+    return nullptr;
+  }
+  if (mRequest->GetCacheMode() != RequestCache::Default) {
+    // Preload can only go with the default caching mode.
+    return nullptr;
+  }
+  if (mRequest->SkipServiceWorker()) {
+    // Preload can't be forbidden interception.
+    return nullptr;
+  }
+  if (mRequest->GetRedirectMode() != RequestRedirect::Follow) {
+    // Preload always follows redirects.
+    return nullptr;
+  }
+  nsAutoCString method;
+  mRequest->GetMethod(method);
+  if (!method.EqualsLiteral("GET")) {
+    // Preload can only do GET, this also eliminates the case we do upload, so
+    // no need to check if the request has any body to send out.
+    return nullptr;
+  }
+
+  // OK, this request can be satisfied by a preloaded response, try to find one.
+
+  // TODO - check if we need to perform step 5 and 6 before using
+  // mRequest->ReferrerPolicy_() here.
+  auto preloadKey =
+      PreloadHashKey::CreateAsFetch(aURI, cors, mRequest->ReferrerPolicy_());
+  return mDocument->Preloads().LookupPreload(&preloadKey);
+}
+
+void FetchDriver::UpdateReferrerInfoFromNewChannel(nsIChannel* aChannel) {
+  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
+  if (!httpChannel) {
+    return;
+  }
+
+  nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo();
+  if (!referrerInfo) {
+    return;
+  }
+
+  nsAutoString computedReferrerSpec;
+  mRequest->SetReferrerPolicy(referrerInfo->ReferrerPolicy());
+  Unused << referrerInfo->GetComputedReferrerSpec(computedReferrerSpec);
+  mRequest->SetReferrer(computedReferrerSpec);
+}
+
 nsresult FetchDriver::Fetch(AbortSignalImpl* aSignalImpl,
                             FetchDriverObserver* aObserver) {
   AssertIsOnMainThread();
 #ifdef DEBUG
   MOZ_ASSERT(!mFetchCalled);
   mFetchCalled = true;
 #endif
 
@@ -433,16 +511,48 @@ nsresult FetchDriver::HttpFetch(
   if (IsBlobURI(uri)) {
     nsAutoCString method;
     mRequest->GetMethod(method);
     if (!method.EqualsLiteral("GET")) {
       return NS_ERROR_DOM_NETWORK_ERR;
     }
   }
 
+  RefPtr<PreloaderBase> fetchPreload = FindPreload(uri);
+  if (fetchPreload) {
+    fetchPreload->RemoveSelf(mDocument);
+    fetchPreload->NotifyUsage(PreloaderBase::LoadBackground::Keep);
+
+    rv = fetchPreload->AsyncConsume(this);
+    if (NS_SUCCEEDED(rv)) {
+      mFromPreload = true;
+
+      mChannel = fetchPreload->Channel();
+      if (mChannel) {
+        // Still in progress, monitor redirects.
+        mChannel->SetNotificationCallbacks(this);
+      }
+
+      // Copied from AsyncOnChannelRedirect.
+      for (const auto& redirect : fetchPreload->Redirects()) {
+        if (redirect.Flags() & nsIChannelEventSink::REDIRECT_INTERNAL) {
+          mRequest->SetURLForInternalRedirect(redirect.Flags(), redirect.Spec(),
+                                              redirect.Fragment());
+        } else {
+          mRequest->AddURL(redirect.Spec(), redirect.Fragment());
+        }
+      }
+
+      return NS_OK;
+    }
+
+    // The preload failed to be consumed.  Behave like there were no preload.
+    fetchPreload = nullptr;
+  }
+
   // Step 2 deals with letting ServiceWorkers intercept requests. This is
   // handled by Necko after the channel is opened.
   // FIXME(nsm): Bug 1119026: The channel's skip service worker flag should be
   // set based on the Request's flag.
 
   // Step 3.1 "If the CORS preflight flag is set and one of these conditions is
   // true..." is handled by the CORS proxy.
   //
@@ -784,16 +894,26 @@ void FetchDriver::FailWithNetworkError(n
 NS_IMETHODIMP
 FetchDriver::OnStartRequest(nsIRequest* aRequest) {
   AssertIsOnMainThread();
 
   // Note, this can be called multiple times if we are doing an opaqueredirect.
   // In that case we will get a simulated OnStartRequest() and then the real
   // channel will call in with an errored OnStartRequest().
 
+  if (mFromPreload && !mChannel) {
+    if (mAborted) {
+      aRequest->Cancel(NS_BINDING_ABORTED);
+      return NS_BINDING_ABORTED;
+    }
+
+    mChannel = do_QueryInterface(aRequest);
+    UpdateReferrerInfoFromNewChannel(mChannel);
+  }
+
   if (!mChannel) {
     MOZ_ASSERT(!mObserver);
     return NS_BINDING_ABORTED;
   }
 
   nsresult rv;
   aRequest->GetStatus(&rv);
   if (NS_FAILED(rv)) {
@@ -1352,29 +1472,17 @@ FetchDriver::AsyncOnChannelRedirect(nsIC
   } else {
     // Overwrite the URL only when the request is redirected by a service
     // worker.
     mRequest->SetURLForInternalRedirect(aFlags, spec, fragment);
   }
 
   // In redirect, httpChannel already took referrer-policy into account, so
   // updates request’s associated referrer policy from channel.
-  if (newHttpChannel) {
-    nsAutoString computedReferrerSpec;
-    nsCOMPtr<nsIReferrerInfo> referrerInfo = newHttpChannel->GetReferrerInfo();
-    if (referrerInfo) {
-      mRequest->SetReferrerPolicy(referrerInfo->ReferrerPolicy());
-      Unused << referrerInfo->GetComputedReferrerSpec(computedReferrerSpec);
-    }
-
-    // Step 8 https://fetch.spec.whatwg.org/#main-fetch
-    // If request’s referrer is not "no-referrer" (empty), set request’s
-    // referrer to the result of invoking determine request’s referrer.
-    mRequest->SetReferrer(computedReferrerSpec);
-  }
+  UpdateReferrerInfoFromNewChannel(aNewChannel);
 
   aCallback->OnRedirectVerifyCallback(NS_OK);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 FetchDriver::CheckListenerChain() { return NS_OK; }
 
@@ -1482,12 +1590,14 @@ void FetchDriver::Abort() {
     mObserver->OnResponseEnd(FetchDriverObserver::eAborted);
     mObserver = nullptr;
   }
 
   if (mChannel) {
     mChannel->Cancel(NS_BINDING_ABORTED);
     mChannel = nullptr;
   }
+
+  mAborted = true;
 }
 
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/fetch/FetchDriver.h
+++ b/dom/fetch/FetchDriver.h
@@ -157,28 +157,42 @@ class FetchDriver final : public nsIStre
   // that these do not overlap.
   bool mNeedToObserveOnDataAvailable;
 
   bool mIsTrackingFetch;
 
   RefPtr<AlternativeDataStreamListener> mAltDataListener;
   bool mOnStopRequestCalled;
 
+  // This flag is true when this fetch has found a matching preload and is being
+  // satisfied by a its response.
+  bool mFromPreload = false;
+  // This flag is set in call to Abort() and spans the possible window this
+  // fetch doesn't have mChannel (to be cancelled) between reuse of the matching
+  // preload, that has already finished and dropped reference to its channel,
+  // and OnStartRequest notification.  It let's us cancel the load when we get
+  // the channel in OnStartRequest.
+  bool mAborted = false;
+
 #ifdef DEBUG
   bool mResponseAvailableCalled;
   bool mFetchCalled;
 #endif
 
   friend class AlternativeDataStreamListener;
 
   FetchDriver() = delete;
   FetchDriver(const FetchDriver&) = delete;
   FetchDriver& operator=(const FetchDriver&) = delete;
   ~FetchDriver();
 
+  already_AddRefed<PreloaderBase> FindPreload(nsIURI* aURI);
+
+  void UpdateReferrerInfoFromNewChannel(nsIChannel* aChannel);
+
   nsresult HttpFetch(
       const nsACString& aPreferredAlternativeDataType = EmptyCString());
   // Returns the filtered response sent to the observer.
   already_AddRefed<InternalResponse> BeginAndGetFilteredResponse(
       InternalResponse* aResponse, bool aFoundOpaqueRedirect);
   // Utility since not all cases need to do any post processing of the filtered
   // response.
   void FailWithNetworkError(nsresult rv);
--- a/uriloader/preload/PreloaderBase.cpp
+++ b/uriloader/preload/PreloaderBase.cpp
@@ -25,16 +25,22 @@ PreloaderBase::RedirectSink::RedirectSin
 NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink, nsIInterfaceRequestor,
                   nsIChannelEventSink, nsIRedirectResultListener)
 
 NS_IMETHODIMP PreloaderBase::RedirectSink::AsyncOnChannelRedirect(
     nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
     nsIAsyncVerifyRedirectCallback* aCallback) {
   mRedirectChannel = aNewChannel;
 
+  // Deliberately adding this before confirmation.
+  nsCOMPtr<nsIURI> uri;
+  aNewChannel->GetOriginalURI(getter_AddRefs(uri));
+  mPreloader->mRedirectRecords.AppendElement(
+      RedirectRecord(aFlags, uri.forget()));
+
   if (mCallbacks) {
     nsCOMPtr<nsIChannelEventSink> sink(do_GetInterface(mCallbacks));
     if (sink) {
       return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags,
                                           aCallback);
     }
   }
 
@@ -108,18 +114,18 @@ void PreloaderBase::NotifyOpen(PreloadHa
   // * Start the usage timer if aIsPreload.
 
   nsCOMPtr<nsIInterfaceRequestor> callbacks;
   mChannel->GetNotificationCallbacks(getter_AddRefs(callbacks));
   RefPtr<RedirectSink> sink(new RedirectSink(this, callbacks));
   mChannel->SetNotificationCallbacks(sink);
 }
 
-void PreloaderBase::NotifyUsage() {
-  if (!mIsUsed && mChannel) {
+void PreloaderBase::NotifyUsage(LoadBackground aLoadBackground) {
+  if (!mIsUsed && mChannel && aLoadBackground == LoadBackground::Drop) {
     nsLoadFlags loadFlags;
     mChannel->GetLoadFlags(&loadFlags);
 
     // Preloads are initially set the LOAD_BACKGROUND flag.  When becoming
     // regular loads by hitting its consuming tag, we need to drop that flag,
     // which also means to re-add the request from/to it's loadgroup to reflect
     // that flag change.
     if (loadFlags & nsIRequest::LOAD_BACKGROUND) {
@@ -139,22 +145,25 @@ void PreloaderBase::NotifyUsage() {
     }
   }
 
   mIsUsed = true;
 
   // * Cancel the usage timer.
 }
 
-void PreloaderBase::NotifyRestart(dom::Document* aDocument,
-                                  PreloaderBase* aNewPreloader) {
+void PreloaderBase::RemoveSelf(dom::Document* aDocument) {
   if (aDocument) {
     aDocument->Preloads().DeregisterPreload(&mKey);
   }
+}
 
+void PreloaderBase::NotifyRestart(dom::Document* aDocument,
+                                  PreloaderBase* aNewPreloader) {
+  RemoveSelf(aDocument);
   mKey = PreloadHashKey();
 
   if (aNewPreloader) {
     aNewPreloader->mNodes = std::move(mNodes);
   }
 }
 
 void PreloaderBase::NotifyStart(nsIRequest* aRequest) {
@@ -224,17 +233,17 @@ void PreloaderBase::RemoveLinkPreloadNod
   nsWeakPtr node = do_GetWeakReference(aNode);
   mNodes.RemoveElement(node);
 
   if (kCancelAndRemovePreloadOnZeroReferences && mNodes.Length() == 0 &&
       !mIsUsed) {
     // Keep a reference, because the following call may release us.  The caller
     // may use a WeakPtr to access this.
     RefPtr<PreloaderBase> self(this);
-    aNode->OwnerDoc()->Preloads().DeregisterPreload(&mKey);
+    RemoveSelf(aNode->OwnerDoc());
 
     if (mChannel) {
       mChannel->Cancel(NS_BINDING_ABORTED);
     }
   }
 }
 
 void PreloaderBase::NotifyNodeEvent(nsINode* aNode) {
@@ -243,9 +252,24 @@ void PreloaderBase::NotifyNodeEvent(nsIN
 }
 
 nsresult PreloaderBase::AsyncConsume(nsIStreamListener* aListener) {
   // We want to return an error so that consumers can't ever use a preload to
   // consume data unless it's properly implemented.
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
+// PreloaderBase::RedirectRecord
+
+nsCString PreloaderBase::RedirectRecord::Spec() const {
+  nsCOMPtr<nsIURI> noFragment;
+  NS_GetURIWithoutRef(mURI, getter_AddRefs(noFragment));
+  MOZ_ASSERT(noFragment);
+  return noFragment->GetSpecOrDefault();
+}
+
+nsCString PreloaderBase::RedirectRecord::Fragment() const {
+  nsCString fragment;
+  mURI->GetRef(fragment);
+  return fragment;
+}
+
 }  // namespace mozilla
--- a/uriloader/preload/PreloaderBase.h
+++ b/uriloader/preload/PreloaderBase.h
@@ -74,20 +74,27 @@ class PreloaderBase : public SupportsWea
   void NotifyValidating();
   // Called when the validation process has been done.  This will notify
   // associated link DOM nodes.
   void NotifyValidated(nsresult aStatus);
 
   // Called by resource loaders or any suitable component to notify the preload
   // has been used for an actual load.  This is intended to stop any usage
   // timers.
-  void NotifyUsage();
+  // @param aDropLoadBackground: If `Keep` then the loading channel, if still in
+  // progress, will not be removed the LOAD_BACKGROUND flag, for instance XHR is
+  // the user here.
+  enum class LoadBackground { Keep, Drop };
+  void NotifyUsage(LoadBackground aLoadBackground = LoadBackground::Drop);
   // Whether this preloader has been used for a regular/actual load or not.
   bool IsUsed() const { return mIsUsed; }
 
+  // Removes itself from the document's preloads hashtable
+  void RemoveSelf(dom::Document* aDocument);
+
   // When a loader starting an actual load finds a preload, the data can be
   // delivered using this method.  It will deliver stream listener notifications
   // as if it were coming from the resource loading channel.  The |request|
   // argument will be the channel that loaded/loads the resource.
   // This method must keep to the nsIChannel.AsyncOpen contract.  A loader is
   // not obligated to re-implement this method when not necessarily needed.
   virtual nsresult AsyncConsume(nsIStreamListener* aListener);
 
@@ -104,16 +111,33 @@ class PreloaderBase : public SupportsWea
   static void AddLoadBackgroundFlag(nsIChannel* aChannel);
 
   // These are linking this preload to <link rel="preload"> DOM nodes.  If we
   // are already loaded, immediately notify events on the node, otherwise wait
   // for NotifyStop() call.
   void AddLinkPreloadNode(nsINode* aNode);
   void RemoveLinkPreloadNode(nsINode* aNode);
 
+  // A collection of redirects, the main consumer is fetch.
+  class RedirectRecord {
+   public:
+    RedirectRecord(uint32_t aFlags, already_AddRefed<nsIURI> aURI)
+        : mFlags(aFlags), mURI(aURI) {}
+
+    uint32_t Flags() const { return mFlags; }
+    nsCString Spec() const;
+    nsCString Fragment() const;
+
+   private:
+    uint32_t mFlags;
+    nsCOMPtr<nsIURI> mURI;
+  };
+
+  const nsTArray<RedirectRecord>& Redirects() { return mRedirectRecords; }
+
  protected:
   virtual ~PreloaderBase();
 
  private:
   void NotifyNodeEvent(nsINode* aNode);
 
   // A helper class that will update the PreloaderBase.mChannel member when a
   // redirect happens, so that we can reprioritize or cancel when needed.
@@ -141,16 +165,19 @@ class PreloaderBase : public SupportsWea
     nsCOMPtr<nsIChannel> mRedirectChannel;
   };
 
  private:
   // Reference to HTMLLinkElement DOM nodes to deliver onload and onerror
   // notifications to.
   nsTArray<nsWeakPtr> mNodes;
 
+  // History of redirects.
+  nsTArray<RedirectRecord> mRedirectRecords;
+
   // The loading channel.  This will update when a redirect occurs.
   nsCOMPtr<nsIChannel> mChannel;
 
   // The key this preload has been registered under.  We want to remember it to
   // be able to deregister itself from the document's preloads.
   PreloadHashKey mKey;
 
   // This overrides the final event we send to DOM nodes to be always 'load'.