GitHub Pages is a fantastic free hosting service for static websites, but it comes with limitations—one being the inability to set custom HTTP headers. This becomes a problem when you need features like SharedArrayBuffer, which requires specific security headers for cross-origin isolation.
In this guide, I'll show you how to work around this limitation using service workers to enable SharedArrayBuffer on GitHub Pages.
Browsers require cross-origin isolation for security-sensitive features like SharedArrayBuffer. This requires two HTTP headers:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
GitHub Pages doesn't allow setting custom headers, so we need a workaround.
Service workers can intercept network requests and modify responses. We'll use this to inject the necessary headers into HTML responses.
Create these two files in your project's public directory:
gh-pages-coi.js
// GitHub Pages specific cross-origin isolation service worker
// If the browser supports SharedArrayBuffer we don't need to do anything
if (typeof SharedArrayBuffer !== 'undefined') {
console.log('SharedArrayBuffer already available, no need for COI service worker');
} else {
// Check if we're already in a cross-origin isolated context
if (window.crossOriginIsolated) {
console.log('Already cross-origin isolated');
} else {
console.log('Not cross-origin isolated, using COI service worker');
// This is a GitHub Pages specific solution that works around the header limitations
const swUrl = new URL('./gh-pages-coi-sw.js', window.location.origin + window.location.pathname);
if ('serviceWorker' in navigator) {
// Register the service worker with the correct scope for GitHub Pages
navigator.serviceWorker.register(swUrl.href, {
// Use the correct scope for GitHub Pages subdirectory deployment
scope: window.location.pathname
}).then(registration => {
console.log('GitHub Pages COI ServiceWorker registered:', registration.scope);
// If the page isn't controlled by the service worker yet, reload to activate it
if (!navigator.serviceWorker.controller) {
console.log('Reloading page to activate service worker...');
window.location.reload();
}
}).catch(error => {
console.error('GitHub Pages COI ServiceWorker registration failed:', error);
});
}
}
}
// Diagnostic function to check isolation status
window.checkCrossOriginIsolation = function() {
const isIsolated = window.crossOriginIsolated === true;
const hasSAB = typeof SharedArrayBuffer === 'function';
console.log('Cross-Origin-Isolated:', isIsolated);
console.log('SharedArrayBuffer available:', hasSAB);
if (hasSAB) {
try {
// Try to create a SharedArrayBuffer to verify it works
const sab = new SharedArrayBuffer(1);
console.log('Successfully created SharedArrayBuffer');
return true;
} catch (e) {
console.error('Failed to create SharedArrayBuffer:', e);
return false;
}
} else {
console.warn('SharedArrayBuffer is not available in this context');
return false;
}
};
// Check isolation status after page load
window.addEventListener('DOMContentLoaded', () => {
setTimeout(window.checkCrossOriginIsolation, 1000);
});
gh-pages-coi-sw.js
// GitHub Pages Cross-Origin Isolation Service Worker
// This service worker adds COOP/COEP headers to enable SharedArrayBuffer
// We need to intercept navigation requests and add the necessary headers
const securityHeaders = {
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin"
};
// Helper to determine if this is a navigation request
function isNavigationRequest(event) {
return event.request.mode === 'navigate';
}
// Helper to check if this is an HTML response
function isHtmlResponse(response) {
const contentType = response.headers.get('Content-Type');
return contentType && contentType.includes('text/html');
}
// Add security headers to the response
function addSecurityHeaders(response) {
// Only add headers to HTML responses
if (!isHtmlResponse(response)) {
return response;
}
// Clone the response to modify its headers
const newHeaders = new Headers(response.headers);
// Add our security headers
Object.entries(securityHeaders).forEach(([header, value]) => {
newHeaders.set(header, value);
});
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
// Install event - take control immediately
self.addEventListener('install', (event) => {
self.skipWaiting();
console.log('GitHub Pages COI ServiceWorker installed');
});
// Activate event - claim clients immediately
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
console.log('GitHub Pages COI ServiceWorker activated');
});
// Fetch event - add headers to navigation requests
self.addEventListener('fetch', (event) => {
// Only process GET requests
if (event.request.method !== 'GET') {
return;
}
// For navigation requests (HTML pages), add security headers
if (isNavigationRequest(event)) {
event.respondWith(
fetch(event.request)
.then(response => addSecurityHeaders(response))
.catch(error => {
console.error('Service worker fetch error:', error);
return new Response('Network error', { status: 500 });
})
);
}
});
Add this script tag to your HTML file(s), ideally in the <head>
section:
<script src="./gh-pages-coi.js"></script>
For a React app created with Create React App, you can add this to public/index.html
. For Vite, add it to index.html
in the root directory.
If you're using GitHub Actions to deploy to GitHub Pages, ensure the service worker files are included in your build output:
name: Deploy to GitHub Pages
on:
push:
branches: ['main']
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: 'pages'
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: npm
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Copy Service Worker Files
run: |
cp public/gh-pages-coi.js dist/
cp public/gh-pages-coi-sw.js dist/
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './dist'
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
gh-pages-coi.js
runs in the browser and checks if SharedArrayBuffer is already available.- If not, it registers the service worker (
gh-pages-coi-sw.js
). - The service worker intercepts navigation requests (page loads).
- It adds the required security headers to HTML responses.
- The browser recognizes these headers and enables cross-origin isolation.
- SharedArrayBuffer becomes available for use.
The script includes a diagnostic function you can call from the browser console:
checkCrossOriginIsolation();
This will log whether cross-origin isolation is active and if SharedArrayBuffer is available.
-
Service worker not registered: Check browser console for errors. Ensure the path to service worker file is correct.
-
Page not reloading: The script should automatically reload the page once to activate the service worker. If this doesn't happen, try clearing your browser cache and hard-reloading.
-
Scope issues: If your site is in a subdirectory on GitHub Pages, ensure the service worker scope matches your deployment path.
-
CORS errors: If you're loading resources from other origins, they must have the
crossorigin
attribute and proper CORS headers.
This service worker hack elegantly solves the problem of enabling SharedArrayBuffer on GitHub Pages without needing server configuration. It's particularly useful for WebAssembly applications, web workers, and other advanced browser features that require cross-origin isolation.
With these simple files added to your project, you can use SharedArrayBuffer and other features requiring cross-origin isolation on GitHub Pages!