Skip to content

Browsing — Headless Browser Abstraction

Granit.Browsing is the framework’s headless-browser abstraction. It plays the same role for browser-driven workloads (HTML→PDF generation, page screenshots, PDF rasterisation, accessibility audits) that Granit.BlobStorage plays for storage and Granit.Imaging plays for image processing: a contracts package plus interchangeable provider implementations.

Typical workloads:

  • HTML → PDF generation (server-side rendered invoices, contracts, reports) through IPdfCapability.
  • PDF → image rasterisation (thumbnails, previews) through IPdfViewerCapability — loads the PDF in Chromium’s native viewer and screenshots per page.
  • Web-page screenshotting and accessibility audits through IBrowserPage + IAccessibilityCapability.
  • Directorysrc
    • DirectoryGranit.Browsing contracts (this package)
      • IHeadlessBrowser.cs
      • IBrowserPage.cs
      • BrowserCapabilities.cs [Flags] enum
      • IHeadlessBrowserPool.cs
      • IBrowserSandboxProfile.cs
      • DirectoryCapabilities/
        • IPdfCapability.cs Chromium-only
        • IPdfViewerCapability.cs Chromium-only
        • IAccessibilityCapability.cs
        • ITracingCapability.cs Playwright-only
        • IHarRecordingCapability.cs Playwright-only
      • DirectoryOptions/
        • GranitBrowsingOptions.cs MaxBrowsers, MaxPagesPerBrowser, …
        • BrowserPageOptions.cs per-page viewport / UA / locale / cookies
        • NavigationOptions.cs
        • ScreenshotOptions.cs
      • DirectoryDiagnostics/
        • BrowsingMetrics.cs meter Granit.Browsing
        • BrowsingActivitySource.cs source Granit.Browsing
    • Granit.Browsing.PuppeteerSharp Chromium provider
    • Granit.Browsing.Playwright Chromium / Firefox / WebKit provider

The provider ecosystem is asymmetric — engines differ. Capabilities are advertised through the BrowserCapabilities flags enum at the IHeadlessBrowser level, and optional functionality lives behind capability interfaces (IPdfCapability, IPdfViewerCapability, …) registered in DI only when the active provider supports them. A consumer that injects IPdfCapability against an unsupported provider fails at boot with a descriptive message rather than at first request.

CapabilityChromium (PuppeteerSharp)Chromium (Playwright)Firefox (Playwright)WebKit (Playwright)
Screenshot
PdfGeneration
PdfViewerNative
NetworkInterception✓ (CDP full)partialpartial
JavaScriptInjection
EmulateDevice
EmulateMedia
AccessibilityTree(deprecated upstream)
TraceRecording
HarRecordingpartial

The AccessibilityTree row is intentionally narrow on Playwright: the .NET client deprecated its tree-export API in favour of ARIA snapshots. Hosts that need the full tree should run on the PuppeteerSharp provider.

Choose one provider per host. Both can technically coexist, but the contracts are registered as singletons — last writer wins.

// PuppeteerSharp — recommended for PDF-heavy workloads (DocumentGeneration,
// Documents.Renditions). Bundled Chromium auto-downloaded on first boot.
services.AddGranitBrowsingPuppeteerSharp(
configureBrowsing: opts =>
{
opts.MaxBrowsers = 2;
opts.MaxPagesPerBrowser = 8;
},
configurePuppeteer: opts =>
{
opts.DisableSandbox = false; // keep the sandbox unless your container can't
// opts.ChromiumExecutablePath = "/usr/bin/chromium";
// opts.SkipChromiumDownload = true; // when bundling Chromium in the image
});
// Microsoft.Playwright — multi-engine, adds tracing + HAR recording.
services.AddGranitBrowsingPlaywright(
configurePlaywright: opts =>
{
opts.Engine = BrowserEngine.Chromium; // or Firefox / Webkit
// opts.SkipBrowserInstall = true; // skip `playwright install` at boot
});

GranitBrowsingOptions drives both providers. Keep these in mind:

  • MaxBrowsers (default 2). Each Chromium instance carries 200–300 MB of resident memory. Bump only when concurrency demands it.
  • MaxPagesPerBrowser (default 8). Pages are cheap; raise this when many short renders contend for browsers.
  • PageMaxLifetime (default 30 min). Hard cap on a page’s age before the pool recycles it. Mitigates Chromium memory leaks accumulating over long uptimes.
  • PageIdleTimeout (default 5 min). Idle pages older than this are recycled out-of-band.
  • AcquireTimeout (default 30 s). AcquirePageAsync surfaces a TimeoutException past this window.

Granit.Browsing ships a deny-by-default IBrowserSandboxProfile that is auto-registered by GranitBrowsingModule. The shipped defaults: HTTPS-only, no private networks, CSP forced, console redacted, 30 s render cap.

Override the profile to tighten the policy further (per-host AllowedHostPatterns, DeniedHostPatterns, shorter MaxRenderDuration):

services.AddSingleton<IBrowserSandboxProfile>(new SandboxProfile
{
AllowedHostPatterns = [ "*.partner.com", "cdn.example.com" ],
DeniedHostPatterns = [ "*.tracker.example" ],
MaxRenderDuration = TimeSpan.FromSeconds(15),
});

Both providers honour the profile through native interception, wrapped by a single per-page RequestRouter that runs sandbox handlers before user RouteAsync registrations. The full threat model, audit events, ConsoleRedactor, PrivilegedFlagGuard, and the five Browsing permissions are covered in Browsing security.

Disabling CSP requires both a sandbox with ForceCsp = false and the Granit.Browsing.Pages.BypassCsp permission on the caller’s role.

  • Meter Granit.Browsing — counters granit.browsing.pages.acquired.count, granit.browsing.pages.released.count, granit.browsing.errors.count; histograms granit.browsing.pool.acquire.duration, granit.browsing.render.duration. Tags: engine, operation, error_type, tenant_id.
  • ActivitySource Granit.Browsing — every page acquisition / navigate / screenshot / PDF render / PDF viewer page is a span. Naming follows browsing.{operation} (browsing.page.acquire, browsing.pdf.render, …).

Both are auto-registered by Granit.Observability through GranitActivitySourceRegistry.

PuppeteerSharp ships a hosted service (PuppeteerChromiumProvisionService) that runs BrowserFetcher.DownloadAsync() at boot when no executable path is configured. Playwright runs playwright install <engine> through PlaywrightProvisionService.

In containerised production, prefer pre-bundling browsers in the image:

# Dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends \
chromium fonts-liberation libxss1 libgbm1 \
&& rm -rf /var/lib/apt/lists/*
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
appsettings.Production.json
{
"Browsing": {
"PuppeteerSharp": {
"ChromiumExecutablePath": "/usr/bin/chromium",
"SkipChromiumDownload": true,
"DisableSandbox": true // common in Docker containers without user namespaces
}
}
}