Skip to content

Instantly share code, notes, and snippets.

@BLamy
Created May 1, 2025 05:06
Show Gist options
  • Save BLamy/0025ded94834009729c57775707cd822 to your computer and use it in GitHub Desktop.
Save BLamy/0025ded94834009729c57775707cd822 to your computer and use it in GitHub Desktop.

Enabling SharedArrayBuffer on GitHub Pages: A Service Worker Solution

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.

The Problem: Cross-Origin Isolation Requirements

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.

The Solution: Service Workers to the Rescue

Service workers can intercept network requests and modify responses. We'll use this to inject the necessary headers into HTML responses.

Implementation Guide

Step 1: Create the Service Worker Files

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 });
        })
    );
  }
});

Step 2: Include the Script in Your HTML

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.

Step 3: Configure Your Build Process (If Using GitHub Actions)

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

How It Works

  1. gh-pages-coi.js runs in the browser and checks if SharedArrayBuffer is already available.
  2. If not, it registers the service worker (gh-pages-coi-sw.js).
  3. The service worker intercepts navigation requests (page loads).
  4. It adds the required security headers to HTML responses.
  5. The browser recognizes these headers and enables cross-origin isolation.
  6. SharedArrayBuffer becomes available for use.

Testing It Works

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.

Common Issues and Solutions

  1. Service worker not registered: Check browser console for errors. Ensure the path to service worker file is correct.

  2. 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.

  3. Scope issues: If your site is in a subdirectory on GitHub Pages, ensure the service worker scope matches your deployment path.

  4. CORS errors: If you're loading resources from other origins, they must have the crossorigin attribute and proper CORS headers.

Conclusion

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment