npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

esi-html-rewriter

v0.6.2

Published

ESI parser for Cloudflare Workers

Readme

ESI Parser for Cloudflare Workers

An ESI parser for Cloudflare Workers using HTMLRewriter. Heavily inspired by cloudflare-esi.

Features

  • ✅ Supports <esi:include src="..."> tags
  • ✅ Uses Cloudflare's HTMLRewriter for efficient streaming parsing
  • ✅ ESI/1.0 compliant: Only processes responses with Surrogate-Control header
  • ✅ Automatic error logging with full error details
  • ✅ Requires html_rewriter_treats_esi_include_as_void_tag compatibility flag

ESI Tag Support

This library only supports esi:include tags. Other ESI tags such as esi:vars, esi:try, esi:choose, esi:when, esi:otherwise, esi:comment, etc. are not supported and will be left unchanged in the HTML.

esi:include Attribute Support

The esi:include tag supports the src attribute (required). The onerror and alt attributes are not supported. However, you can control error handling behavior using the onError option (see Error Handling section below).

Usage

Basic Usage

import { Esi } from "esi-html-rewriter";

const esi = new Esi({ shim: true });

// Process a Response (ESI/1.0 compliant - only processes if Surrogate-Control header is present)
// Esi.handleRequest automatically adds the Surrogate-Capability header to advertise ESI support
const request = new Request("https://example.com/page");
const processedResponse = await esi.handleRequest(request);

// Or process an existing Response
const response = new Response(html, {
  headers: {
    "Content-Type": "text/html",
    "Surrogate-Control": 'content="ESI/1.0"',
  },
});
const processed = await esi.parseResponse(response, [request]);

ESI/1.0 Compliance

This library follows the ESI/1.0 specification for surrogate control:

  1. Advertise capabilities: The handleRequest() method automatically adds the Surrogate-Capability header to requests, advertising that your worker can process ESI.

  2. Check for delegation: When surrogateDelegation is enabled, the parseResponse() method checks if downstream surrogates can handle ESI processing. If so, it delegates the response without processing.

  3. Process only delegated content: The parseResponse() method only processes Response objects that include a Surrogate-Control header with content="ESI/1.0", indicating the origin server has delegated ESI processing to your worker.

  4. Content type filtering: By default, only responses with Content-Type: text/html are processed. You can customize this with the contentTypes option.

  5. Skip processing: If the response doesn't meet the requirements (missing Surrogate-Control header or unsupported content type), parseResponse() returns the original response unchanged.

// In your Cloudflare Worker
import { Esi } from "esi-html-rewriter";

export default {
  async fetch(request: Request): Promise<Response> {
    const esi = new Esi({ shim: true });

    // Fetch and process (automatically adds Surrogate-Capability header)
    // Only processes if response has Surrogate-Control: content="ESI/1.0"
    return esi.handleRequest(request);
  },
};

Configuration

The parser supports the following options:

  • contentTypes: Array of content types that should be processed for ESI includes (default: ['text/html', 'text/plain'])
  • maxDepth: Maximum recursion depth for nested ESI includes (default: 5)
  • allowedUrlPatterns: Array of URLPattern objects to restrict which URLs can be included (default: [new URLPattern()] - allows all URLs)
  • shim: When true, replaces <esi:include /> tags with <esi-include></esi-include> to work around compatibility flag issues (default: false)
  • onError: Optional callback function (error: unknown, element: Element) => void for custom error handling. Default behavior removes the element and logs the error.
  • surrogateDelegation: Surrogate Delegation - if true and the request has valid Surrogate-Capability headers indicating a downstream surrogate can handle ESI, the response will be returned without processing. If an array of strings, each string is treated as an IP address. Delegation only occurs if the connecting IP (CF-Connecting-IP) matches one of the provided IPs. (default: false)
  • surrogateControlHeader: Name of the header to check for Surrogate-Control. Useful when Cloudflare prioritizes Surrogate-Control over Cache-Control. (default: "Surrogate-Control")
  • fetch: Custom fetch function to override the default global fetch. Has the same signature as the global fetch function. (default: uses global fetch)

Error Handling

When an ESI include fails (network error, 404, etc.), by default the error is logged to console.error with full details (including error code, cause, stack trace, etc.) and the element is removed from the HTML. This allows processing to continue for other ESI includes even if one fails.

Note: The onerror and alt attributes on esi:include tags are not supported. Use the onError option instead to customize error handling behavior.

You can customize error handling by providing an onError callback:

const esi = new Esi({
  shim: true,
  onError: (error, element) => {
    // Custom error handling - replace with error message, log to external service, etc.
    element.replace(
      `<!-- Error: ${error instanceof Error ? error.message : String(error)} -->`,
      { html: true },
    );
  },
});

Security and Recursion Examples

import { Esi } from "esi-html-rewriter";

// Limit recursion depth to prevent infinite loops
const esi1 = new Esi({ maxDepth: 5, shim: true });
const request1 = new Request("https://example.com/page");
const result1 = await esi1.handleRequest(request1);

// Restrict which URLs can be included using URLPattern
const esi2 = new Esi({
  allowedUrlPatterns: [
    new URLPattern({ pathname: "/api/*" }),
    new URLPattern({ origin: "https://trusted-domain.com", pathname: "/*" }),
  ],
  shim: true,
});
const request2 = new Request("https://example.com/page");
const result2 = await esi2.handleRequest(request2);

// Combine security options
const esi3 = new Esi({
  maxDepth: 2,
  allowedUrlPatterns: [
    new URLPattern({ pathname: "/api/*" }),
    new URLPattern({ pathname: "/static/*" }),
  ],
  shim: true,
});
const request3 = new Request("https://example.com/page");
const result3 = await esi3.handleRequest(request3);

// Custom error handling
const esi4 = new Esi({
  shim: true,
  onError: (error, element) => {
    console.error("ESI include failed:", error);
    element.replace("<!-- ESI include failed -->", { html: true });
  },
});

// Enable surrogate delegation
const esi5 = new Esi({
  shim: true,
  surrogateDelegation: true, // Delegate to downstream surrogates when possible
});

// Enable surrogate delegation with IP restrictions
const esi6 = new Esi({
  shim: true,
  surrogateDelegation: ["192.168.1.1", "10.0.0.1"], // Only delegate from these IPs
});

Testing

Tests use Vitest with @cloudflare/vitest-pool-workers:

npm test

Development

# Run tests in watch mode
npm run test:watch

# Run development server
npm run dev

# Deploy to Cloudflare
npm run deploy

Requirements

  • Cloudflare Workers runtime
  • html_rewriter_treats_esi_include_as_void_tag compatibility flag enabled in wrangler.jsonc

Known Issues

Compatibility Flag Not Applied

There is a known bug where the html_rewriter_treats_esi_include_as_void_tag compatibility flag is not being applied in workerd, affecting both development (wrangler dev) and deployed workers. This causes HTMLRewriter to throw a TypeError: Parser error: Unsupported pseudo-class or pseudo-element in selector when trying to use esi:include as an element selector.

Workaround: Enable the shim option to automatically replace <esi:include /> tags with <esi-include></esi-include> before processing. This allows the library to work around the compatibility flag issue:

const esi = new Esi({ shim: true });
const request = new Request("https://example.com/page");
const processed = await esi.handleRequest(request);
// or
const processed = await esi.parseResponse(response, [request]);