esi-html-rewriter
v0.6.2
Published
ESI parser for Cloudflare Workers
Maintainers
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-Controlheader - ✅ Automatic error logging with full error details
- ✅ Requires
html_rewriter_treats_esi_include_as_void_tagcompatibility 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:
Advertise capabilities: The
handleRequest()method automatically adds theSurrogate-Capabilityheader to requests, advertising that your worker can process ESI.Check for delegation: When
surrogateDelegationis enabled, theparseResponse()method checks if downstream surrogates can handle ESI processing. If so, it delegates the response without processing.Process only delegated content: The
parseResponse()method only processes Response objects that include aSurrogate-Controlheader withcontent="ESI/1.0", indicating the origin server has delegated ESI processing to your worker.Content type filtering: By default, only responses with
Content-Type: text/htmlare processed. You can customize this with thecontentTypesoption.Skip processing: If the response doesn't meet the requirements (missing
Surrogate-Controlheader 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 ofURLPatternobjects to restrict which URLs can be included (default:[new URLPattern()]- allows all URLs)shim: Whentrue, 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) => voidfor custom error handling. Default behavior removes the element and logs the error.surrogateDelegation: Surrogate Delegation - iftrueand the request has validSurrogate-Capabilityheaders 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 globalfetch)
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 testDevelopment
# Run tests in watch mode
npm run test:watch
# Run development server
npm run dev
# Deploy to Cloudflare
npm run deployRequirements
- Cloudflare Workers runtime
html_rewriter_treats_esi_include_as_void_tagcompatibility flag enabled inwrangler.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]);