Cross-Origin Service Workers – Tests mit Foreign Fetch

Jan Posnick
Jeff Posnick

Hintergrund

Service Worker geben Webentwicklern die Möglichkeit, auf Netzwerkanfragen von ihren Webanwendungen zu reagieren. So können sie auch offline weiterarbeiten, Lie-Fi bekämpfen und komplexe Cache-Interaktionen wie stale-while-revalid implementieren. In der Vergangenheit waren Service Worker an einen bestimmten Ursprung gebunden. Als Inhaber einer Webanwendung sind Sie dafür verantwortlich, einen Service Worker zu schreiben und bereitzustellen, der alle Netzwerkanfragen Ihrer Webanwendung abfängt. Bei diesem Modell ist jeder Service Worker auch für die Verarbeitung von ursprungsübergreifenden Anfragen verantwortlich, beispielsweise an eine Drittanbieter-API oder an Webschriftarten.

Was wäre, wenn ein Drittanbieter einer API, von Webschriftarten oder eines anderen häufig verwendeten Dienstes die Möglichkeit hätte, einen eigenen Service Worker bereitzustellen, der die Möglichkeit hätte, Anfragen von anderen Ursprüngen zu verarbeiten? Anbieter können ihre eigene benutzerdefinierte Netzwerklogik implementieren und eine einzelne, autoritative Cache-Instanz zum Speichern ihrer Antworten nutzen. Dank des Fremdabrufs werden Service Workers von Drittanbietern bereitgestellt.

Die Bereitstellung eines Service Workers, der fremde Abrufe implementiert, ist für alle Anbieter eines Dienstes sinnvoll, auf die über HTTPS-Anfragen von Browsern zugegriffen wird. Denken Sie nur an Szenarien, in denen Sie eine netzwerkunabhängige Version Ihres Dienstes bereitstellen könnten, bei der Browser einen gemeinsamen Ressourcen-Cache nutzen könnten. Folgende Dienste können unter anderem von diesen Vorteilen profitieren:

  • API-Anbieter mit RESTful
  • Anbieter von Webschriftarten
  • Analyseanbieter
  • Bildhosting-Anbieter
  • Allgemeine Content Delivery Networks

Stellen Sie sich vor, Sie sind ein Analyseanbieter. Durch die Bereitstellung eines externen Abrufdienst-Workers können Sie dafür sorgen, dass alle Anfragen an Ihren Dienst, die fehlschlagen, während ein Nutzer offline ist, in die Warteschlange gestellt und wiederholt werden, sobald die Verbindung wiederhergestellt ist. Es ist zwar möglich, dass die Clients eines Dienstes ein ähnliches Verhalten über eigene Service Worker implementieren, aber jeder einzelne Client muss eine maßgeschneiderte Logik für Ihren Dienst schreiben. Das ist allerdings nicht so skalierbar wie die Nutzung eines freigegebenen fremden Abrufdienst-Workers, den Sie bereitstellen.

Voraussetzungen

Ursprüngliches Testtoken

Foreign Fetch gilt immer noch als experimentell. Um zu vermeiden, dass dieses Design verfrüht integriert wird, bevor es vollständig spezifiziert und von den Browseranbietern vereinbart wurde, wurde es in Chrome 54 als Ursprungstest implementiert. Solange der Fremdabruf noch experimentell ist, müssen Sie ein Token anfordern, das auf den spezifischen Ursprung Ihres Dienstes ausgerichtet ist, um diese neue Funktion mit dem von Ihnen gehosteten Dienst verwenden zu können. Das Token sollte als HTTP-Antwortheader in alle ursprungsübergreifenden Anfragen für Ressourcen aufgenommen werden, die Sie über einen externen Abruf verarbeiten möchten, sowie in der Antwort für Ihre Service Worker-JavaScript-Ressource:

Origin-Trial: token_obtained_from_signup

Der Testzeitraum endet im März 2017. Wir gehen davon aus, dass wir alle notwendigen Änderungen zur Stabilisierung der Funktion identifiziert und diese (hoffentlich) standardmäßig aktiviert haben. Wenn der Fremdabruf bis dahin nicht standardmäßig aktiviert ist, funktioniert die Funktion für vorhandene Ursprungstests-Tokens nicht mehr.

Um das Experimentieren mit ausländischen Abrufvorgängen vor der Registrierung eines offiziellen Ursprungstests-Tokens zu erleichtern, können Sie die Anforderung in Chrome für Ihren lokalen Computer umgehen. Rufen Sie dazu chrome://flags/#enable-experimental-web-platform-features auf und aktivieren Sie das Flag „Experimental Web Platform features“ (Experimentelle Webplattformfunktionen). Dies muss in jeder Instanz von Chrome erfolgen, die Sie in Ihren lokalen Tests verwenden möchten. Mit einem Ursprungstest-Token ist die Funktion hingegen für alle Ihre Chrome-Nutzer verfügbar.

HTTPS

Wie bei allen Service Worker-Bereitstellungen muss der Zugriff auf den Webserver, den Sie für die Bereitstellung Ihrer Ressourcen verwenden, und das Service Worker-Skript über HTTPS erfolgen. Außerdem gilt das Abfangen von ausländischen Abruf nur für Anfragen von Seiten, die auf sicheren Ursprüngen gehostet werden. Die Clients Ihres Dienstes müssen also HTTPS verwenden, um Ihre fremde Abrufimplementierung nutzen zu können.

Foreign Fetch verwenden

Sehen wir uns nun die technischen Details an, die erforderlich sind, um einen externen Abrufdienst-Worker einzurichten und auszuführen.

Service Worker registrieren

Die erste Herausforderung, auf die Sie wahrscheinlich stoßen werden, ist die Registrierung Ihres Service Workers. Wenn Sie bereits mit Service Workern gearbeitet haben, sind Sie wahrscheinlich mit Folgendem vertraut:

// You can't do this!
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
}

Dieser JavaScript-Code für die Registrierung von Service Workern als Erstanbieter ist im Kontext einer Web-App sinnvoll. Er wird ausgelöst, wenn ein Nutzer zu einer von Ihnen verwalteten URL navigiert. Es ist jedoch kein praktikabler Ansatz für die Registrierung eines Service Workers von Drittanbietern, wenn die einzige Interaktion des Browsers mit Ihrem Server darin besteht, eine bestimmte Unterressource anzufordern, keine vollständige Navigation. Wenn der Browser beispielsweise ein Bild von einem von Ihnen verwalteten CDN-Server anfordert, können Sie dieses JavaScript-Snippet nicht Ihrer Antwort voranstellen und erwarten, dass es ausgeführt wird. Eine andere Methode für die Service Worker-Registrierung ist außerhalb des normalen JavaScript-Ausführungskontexts erforderlich.

Die Lösung besteht in Form eines HTTP-Headers, den Ihr Server in jede Antwort aufnehmen kann:

Link: </service-worker.js>; rel="serviceworker"; scope="/"

Wir schlüsseln diesen Beispielheader in seine Komponenten auf, die alle durch ein ;-Zeichen getrennt sind.

  • </service-worker.js> ist erforderlich und wird verwendet, um den Pfad zu Ihrer Service Worker-Datei anzugeben. Ersetzen Sie dabei /service-worker.js durch den entsprechenden Pfad zu Ihrem Skript. Dies entspricht direkt dem String scriptURL, der andernfalls als erster Parameter an navigator.serviceWorker.register() übergeben würde. Der Wert muss gemäß der Link-Header-Spezifikation in <>-Zeichen enthalten sein. Wird eine relative statt einer absoluten URL angegeben, wird sie als relativ zur Position der Antwort interpretiert.
  • rel="serviceworker" ist ebenfalls erforderlich und sollte ohne Anpassung enthalten sein.
  • scope=/ ist eine optionale Bereichsdeklaration, was dem String options.scope entspricht, den Sie als zweiten Parameter an navigator.serviceWorker.register() übergeben können. In vielen Anwendungsfällen ist die Verwendung des Standardumfangs für Sie ausreichend. Lassen Sie dies also weg, sofern Sie nicht wissen, dass Sie ihn benötigen. Für Link-Header-Registrierungen gelten die gleichen Einschränkungen hinsichtlich des maximal zulässigen Bereichs sowie die Möglichkeit, diese Einschränkungen über den Service-Worker-Allowed-Header zu lockern.

Genau wie bei einer „traditionellen“ Service Worker-Registrierung wird durch die Verwendung des Link-Headers ein Service Worker installiert, der für die nächste Anfrage für den registrierten Bereich verwendet wird. Der Text der Antwort, der den speziellen Header enthält, wird unverändert verwendet und ist sofort für die Seite verfügbar, ohne darauf warten zu müssen, dass der Fremddienstmitarbeiter die Installation abgeschlossen hat.

Da der Abruf aus anderen Quellen derzeit als Ursprungstest implementiert wird, musst du neben dem Header der Linkantwort auch einen gültigen Origin-Trial-Header angeben. Die Mindestanzahl von Antwortheadern, die hinzugefügt werden müssen, um deinen fremdsprachigen Abrufdienst-Worker zu registrieren, ist

Link: </service-worker.js>; rel="serviceworker"
Origin-Trial: token_obtained_from_signup

Fehler bei der Registrierung beheben

Während der Entwicklung sollten Sie überprüfen, ob Ihr fremder Abrufdienst-Worker ordnungsgemäß installiert ist und Anfragen verarbeitet. In den Entwicklertools von Chrome können Sie überprüfen, ob alles wie erwartet funktioniert.

Werden die richtigen Antwortheader gesendet?

Um den externen Abrufdienst-Worker zu registrieren, müssen Sie einen Link-Header für eine Antwort auf eine auf Ihrer Domain gehostete Ressource festlegen, wie weiter oben in diesem Post beschrieben. Während des Ursprungstests müssen Sie, sofern chrome://flags/#enable-experimental-web-platform-features nicht festgelegt ist, auch einen Origin-Trial-Antwortheader festlegen. Sie können überprüfen, ob Ihr Webserver diese Header festlegt, indem Sie sich den Eintrag im Bereich Netzwerk der Entwicklertools ansehen:

Header, die im Steuerfeld „Netzwerk“ angezeigt werden.

Ist der Foreign Fetch Service Worker ordnungsgemäß registriert?

Sie können auch die zugrunde liegende Service Worker-Registrierung einschließlich ihres Umfangs prüfen, indem Sie sich die vollständige Liste der Service Worker im Bereich Anwendung der Entwicklertools ansehen. Wählen Sie die Option „Alle anzeigen“ aus, da standardmäßig nur Service Worker für den aktuellen Ursprung angezeigt werden.

Der fremde Abrufdienst-Worker im Bereich „Anwendungen“.

Event-Handler für die Installation

Nachdem Sie den externen Service Worker registriert haben, kann er auf die Ereignisse install und activate antworten, genau wie jeder andere Service Worker. Die Funktion kann diese Ereignisse nutzen, um beispielsweise während des install-Ereignisses Caches mit den erforderlichen Ressourcen zu füllen oder veraltete Caches im activate-Ereignis zu bereinigen.

Neben den üblichen Cache-Aktivitäten für install-Ereignisse gibt es einen zusätzlichen Schritt im install-Event-Handler des externen Service Workers. Ihr Code muss registerForeignFetch() wie im folgenden Beispiel aufrufen:

self.addEventListener('install', event => {
    event.registerForeignFetch({
    scopes: [self.registration.scope], // or some sub-scope
    origins: ['*'] // or ['https://example.com']
    });
});

Es gibt zwei Konfigurationsoptionen, die beide erforderlich sind:

  • scopes verwendet ein Array aus einem oder mehreren Strings, von denen jeder einen Bereich für Anfragen darstellt, die ein foreignfetch-Ereignis auslösen. Moment mal, vielleicht denken Sie: Ich habe bei der Service Worker-Registrierung bereits einen Umfang definiert. Das stimmt und der Gesamtumfang ist dennoch relevant – jeder hier angegebene Umfang muss entweder dem Gesamtumfang des Service Workers entsprechen oder nur einem Teil davon entsprechen. Mit den zusätzlichen Bereichseinschränkungen können Sie einen Allzweck-Service Worker bereitstellen, der sowohl Erstanbieter-fetch-Ereignisse (für Anfragen von Ihrer eigenen Website) als auch foreignfetch-Drittanbieter-Ereignisse (für Anfragen von anderen Domains) verarbeiten kann und deutlich macht, dass nur ein Teil Ihres größeren Bereichs foreignfetch auslösen sollte. Wenn Sie in der Praxis einen Service Worker bereitstellen, der ausschließlich für die Verarbeitung von foreignfetch-Ereignissen von Drittanbietern vorgesehen ist, empfiehlt es sich, nur einen einzelnen, expliziten Bereich zu verwenden, der dem Gesamtbereich Ihres Service Workers entspricht. Dazu wird im obigen Beispiel der Wert self.registration.scope verwendet.
  • origins verwendet auch ein Array mit einem oder mehreren Strings und ermöglicht es Ihnen, den foreignfetch-Handler so zu beschränken, dass er nur auf Anfragen von bestimmten Domains antwortet. Wenn du beispielsweise „https://beispiel.de“ ausdrücklich zulässt, löst eine Anfrage von einer unter https://example.com/path/to/page.html gehosteten Seite für eine Ressource, die von deinem fremden Abrufbereich bereitgestellt wird, deinen fremdsprachigen Abruf-Handler aus. Anfragen von https://random-domain.com/path/to/page.html lösen deinen Handler jedoch nicht aus. Sofern Sie Ihre fremde Abruflogik nicht nur für einen Teil der Remote-Ursprünge auslösen möchten, können Sie einfach '*' als einzigen Wert im Array angeben. Alle Ursprünge sind dann zulässig.

Ereignis-Handler für Fremdabruf

Nachdem Sie den externen Service Worker installiert und über registerForeignFetch() konfiguriert haben, kann er ursprungsübergreifende Anfragen zu Unterressourcen an Ihren Server abfangen, die in den fremden Abrufbereich fallen.

Bei einem herkömmlichen eigenen Service Worker würde jede Anfrage ein fetch-Ereignis auslösen, auf das der Service Worker antworten konnte. Unser Service Worker (Drittanbieter) hat die Möglichkeit, ein geringfügig anderes Ereignis namens foreignfetch zu verarbeiten. Vom Konzept her sind die beiden Ereignisse sehr ähnlich. Sie geben Ihnen die Möglichkeit, die eingehende Anfrage zu prüfen und optional über respondWith() eine Antwort darauf zu senden:

self.addEventListener('foreignfetch', event => {
    // Assume that requestLogic() is a custom function that takes
    // a Request and returns a Promise which resolves with a Response.
    event.respondWith(
    requestLogic(event.request).then(response => {
        return {
        response: response,
        // Omit to origin to return an opaque response.
        // With this set, the client will receive a CORS response.
        origin: event.origin,
        // Omit headers unless you need additional header filtering.
        // With this set, only Content-Type will be exposed.
        headers: ['Content-Type']
        };
    })
    );
});

Trotz der konzeptionellen Ähnlichkeiten gibt es in der Praxis einige Unterschiede beim Aufrufen von respondWith() für ein ForeignFetchEvent. Anstatt einfach ein Response (oder Promise, das mit einem Response aufgelöst wird) wie mit einem FetchEvent anzugeben, musst du eine Promise übergeben, die mit einem Objekt mit bestimmten Eigenschaften auf die respondWith() des ForeignFetchEvent aufgelöst wird:respondWith()

  • response ist erforderlich und muss auf das Response-Objekt festgelegt werden, das an den Client zurückgegeben wird, der die Anfrage gestellt hat. Wenn du andere Werte als eine gültige Response angibst, wird die Clientanfrage mit einem Netzwerkfehler beendet. Anders als beim Aufrufen von respondWith() in einem fetch-Event-Handler müssen Sie hier einen Response angeben und keinen Promise, der mit Response aufgelöst wird. Sie können Ihre Antwort über eine Promise-Kette erstellen und diese Kette als Parameter an respondWith() von foreignfetch übergeben. Die Kette muss jedoch mit einem Objekt aufgelöst werden, das die response-Eigenschaft und ein Response-Objekt enthält. Ein Beispiel dafür finden Sie im obigen Codebeispiel.
  • origin ist optional und bestimmt, ob die zurückgegebene Antwort opaque ist. Wenn Sie diesen Wert weglassen, ist die Antwort intransparent und der Client hat nur eingeschränkten Zugriff auf den Text und die Header der Antwort. Wenn die Anfrage mit mode: 'cors' erfolgte, wird die Rückgabe einer intransparenten Antwort als Fehler behandelt. Wenn Sie jedoch einen Stringwert angeben, der dem Ursprung des Remoteclients entspricht (der über event.origin abgerufen werden kann), wird explizit die Bereitstellung einer CORS-fähigen Antwort an den Client aktiviert.
  • headers ist ebenfalls optional und nur nützlich, wenn Sie auch origin angeben und eine CORS-Antwort zurückgeben. Standardmäßig werden nur Header aus der Liste der Antwortheader mit CORS-sicherer Liste in Ihre Antwort aufgenommen. Wenn Sie weiter filtern möchten, was zurückgegeben wird, wird für Header eine Liste mit einem oder mehreren Headernamen verwendet, die als Zulassungsliste für die Header verwendet wird, die in der Antwort angezeigt werden sollen. So können Sie CORS aktivieren und gleichzeitig verhindern, dass potenziell vertrauliche Antwortheader direkt für den Remoteclient freigegeben werden.

Wichtig: Wenn der foreignfetch-Handler ausgeführt wird, hat er Zugriff auf alle Anmeldedaten und Umgebungsbefugnisse des Ursprungs, der den Service Worker hostet. Wenn Sie als Entwickler einen Service Worker mit aktiviertem Abruf aus einem anderen Land bereitstellen, müssen Sie dafür sorgen, dass keine privilegierten Antwortdaten offengelegt werden, die sonst aufgrund dieser Anmeldedaten nicht verfügbar wären. Das Erfordern einer Zustimmung für CORS-Antworten ist nur ein Schritt, um eine unbeabsichtigte Offenlegung zu begrenzen. Als Entwickler können Sie jedoch fetch()-Anfragen in Ihrem foreignfetch-Handler explizit fetch() senden, ohne die impliziten Anmeldedaten zu verwenden:

self.addEventListener('foreignfetch', event => {
    // The new Request will have credentials omitted by default.
    const noCredentialsRequest = new Request(event.request.url);
    event.respondWith(
    // Replace with your own request logic as appropriate.
    fetch(noCredentialsRequest)
        .catch(() => caches.match(noCredentialsRequest))
        .then(response => ({response}))
    );
});

Überlegungen zu Clients

Es gibt einige zusätzliche Überlegungen, die sich darauf auswirken, wie Ihr fremder Abrufdienst-Worker Anfragen von Clients Ihres Dienstes verarbeitet.

Kunden mit einem eigenen First-Party Service Worker

Einige Clients Ihres Dienstes haben möglicherweise bereits einen eigenen First-Party-Service-Worker, der Anfragen aus ihrer Webanwendung verarbeitet. Was bedeutet das für Ihren externen, fremden Abrufdienst-Worker?

Die fetch-Handler in einem eigenen Service Worker haben die erste Gelegenheit, auf alle Anfragen von der Web-App zu antworten, auch wenn es einen Service Worker eines Drittanbieters gibt, bei dem foreignfetch aktiviert ist und der Bereich die Anfrage abdeckt. Kunden mit eigenen Service Workern können jedoch trotzdem von Ihrem fremden Abrufdienst-Worker profitieren.

In einem eigenen Service Worker wird durch die Verwendung von fetch() zum Abrufen von ursprungsübergreifenden Ressourcen der entsprechende fremde Abrufdienst-Worker ausgelöst. Das bedeutet, dass Code wie der folgende Ihren foreignfetch-Handler nutzen kann:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    // If event.request is under your foreign fetch service worker's
    // scope, this will trigger your foreignfetch handler.
    event.respondWith(fetch(event.request));
});

Ähnlich verhält es sich, wenn eigene Abruf-Handler vorhanden sind, die event.respondWith() bei der Verarbeitung von Anfragen für deine ursprungsübergreifende Ressource jedoch nicht aufrufen. In diesem Fall „durchläuft“ die Anfrage automatisch deinen foreignfetch-Handler:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    if (event.request.mode === 'same-origin') {
    event.respondWith(localRequestLogic(event.request));
    }

    // Since event.respondWith() isn't called for cross-origin requests,
    // any foreignfetch handlers scoped to the request will get a chance
    // to provide a response.
});

Wenn ein eigener fetch-Handler event.respondWith() aufruft, aber fetch() nicht verwendet, um eine Ressource in Ihrem fremden Abrufbereich anzufordern, hat Ihr fremder Abrufdienst-Worker keine Möglichkeit, die Anfrage zu verarbeiten.

Clients ohne eigenen Service Worker

Alle Clients, die Anfragen an einen Drittanbieterdienst senden, können von der Bereitstellung eines externen Abrufdienst-Workers profitieren, auch wenn sie nicht bereits ihren eigenen Service Worker verwenden. Clients müssen nichts unternehmen, um die Verwendung eines externen Abrufdienst-Workers zu aktivieren, solange sie einen Browser verwenden, der dies unterstützt. Das bedeutet, dass durch die Bereitstellung eines fremdsprachigen Abrufdienst-Workers Ihre benutzerdefinierte Anforderungslogik und der gemeinsam genutzte Cache viele Clients Ihres Dienstes sofort nutzen, ohne dass diese weitere Schritte ausführen müssen.

Zusammenfassung: Wo Kunden nach einer Antwort suchen

Unter Berücksichtigung der oben genannten Informationen können wir eine Hierarchie von Quellen zusammenstellen, die ein Client verwendet, um eine Antwort auf eine ursprungsübergreifende Anfrage zu finden.

  1. Der fetch-Handler eines eigenen Service Workers (falls vorhanden)
  2. Der foreignfetch-Handler eines Service Workers eines Drittanbieters (falls vorhanden und nur für ursprungsübergreifende Anfragen)
  3. Den HTTP-Cache des Browsers (falls eine neue Antwort vorhanden ist)
  4. Das Netzwerk

Der Browser beginnt von oben nach unten und fährt je nach Service Worker-Implementierung von oben nach unten, bis er eine Quelle für die Antwort findet.

Weitere Informationen

Bleibe auf dem neuesten Stand

Die Implementierung des Ursprungstests für den ausländischen Abruf in Chrome kann sich ändern, da wir auf Feedback von Entwicklern eingehen. Der Beitrag wird durch Inline-Änderungen auf dem neuesten Stand gehalten und die unten aufgeführten Änderungen werden in Kürze vorgenommen. Informationen zu wichtigen Änderungen senden wir auch über das Twitter-Konto @chromiumdev.