Issue
When running the Javascript SDK inside Service Worker, the SDK Streaming http call to streaming.split.io is blocked by CORS browser policy as shown below:
Root cause
The Service Worker acts as a proxy between the browser and the network. By intercepting requests made by the document, service workers can redirect requests to a cache, enabling offline access.
A request interception occurs and the application that makes use of this technology requires definitions about how to handle certain requests (fetch) and return a result to the browser/DOM.
In the case of the JS SDK, the Service Worker can receive Push Notifications, which are the specific type of connection called Server-Side Events (SSE). These notifications must be defined into the Service Worker to allow the Stream connection between our SDK and Split's backend to work.
If you use a Service Worker as a proxy, and you hook the fetch request in a particular way (e.g., adding the cache-control
header) but don't take the SSE types into consideration, it might lead to a CORS issue.
Answer
The following is an example of what you can do to manipulate the SSE streams connections (only):
self.addEventListener('fetch', event => {
const {headers, url} = event.request;
const isSSERequest = headers.get('Accept') === 'text/event-stream';
// We process only SSE connections
if (!isSSERequest) {
return;
}
// Response Headers for SSE
const sseHeaders = {
'content-type': 'text/event-stream',
'Transfer-Encoding': 'chunked',
'Connection': 'keep-alive',
};
// Function formatting data for SSE
const sseChunkData = (data, event, retry, id) =>
Object.entries({event, id, data, retry})
.filter(([, value]) => ![undefined, null].includes(value))
.map(([key, value]) => `${key}: ${value}`)
.join('\n') + '\n\n';
// Table with server connections, where key is url, value is EventSource
const serverConnections = {};
// For each url, we open only one connection to the server and use it for subsequent requests
const getServerConnection = url => {
if (!serverConnections[url]) serverConnections[url] = new EventSource(url);
return serverConnections[url];
};
// When we receive a message from the server, we forward it to the browser
const onServerMessage = (controller, {data, type, retry, lastEventId}) => {
const responseText = sseChunkData(data, type, retry, lastEventId);
const responseData = Uint8Array.from(responseText, x => x.charCodeAt(0));
controller.enqueue(responseData);
};
const stream = new ReadableStream({
start: controller => getServerConnection(url).onmessage = onServerMessage.bind(null, controller)
});
const response = new Response(stream, {headers: sseHeaders});
event.respondWith(response);
});
Note: If a defaultHandler is set to NetworkFirst [setDefaultHandler(newNetworkFirst());], that could prevent the event listener from firing. Removing the default handler fixes this.
An alternate example returns false from the listener as shown below:
self.addEventListener('fetch', event => {
// no caching for chrome-extensions
if (event.request.url.startsWith('chrome-extension:')) {
return false;
}
// prevent header striping errors from workbox strategies for EventSource types
if (event.request.url.includes('streaming.split.io')) {
return false;
}
// prevent non-cacheable post requests
if (event.request.method != 'GET') {
return false;
}
// all others, use NetworkFirst workbox strategies
if (strategies) {
const networkFirst = new strategies.NetworkFirst();
event.respondWith(networkFirst.handle({ request: event.request }));
}
});
Comments
0 comments
Please sign in to leave a comment.