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.
Package structure
Section titled “Package structure”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
- BrowsingMetrics.cs meter
- Granit.Browsing.PuppeteerSharp Chromium provider
- Granit.Browsing.Playwright Chromium / Firefox / WebKit provider
Capability matrix
Section titled “Capability matrix”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.
| Capability | Chromium (PuppeteerSharp) | Chromium (Playwright) | Firefox (Playwright) | WebKit (Playwright) |
|---|---|---|---|---|
Screenshot | ✓ | ✓ | ✓ | ✓ |
PdfGeneration | ✓ | ✓ | — | — |
PdfViewerNative | ✓ | ✓ | — | — |
NetworkInterception | ✓ (CDP full) | ✓ | partial | partial |
JavaScriptInjection | ✓ | ✓ | ✓ | ✓ |
EmulateDevice | ✓ | ✓ | ✓ | ✓ |
EmulateMedia | ✓ | ✓ | ✓ | ✓ |
AccessibilityTree | ✓ | — (deprecated upstream) | — | — |
TraceRecording | — | ✓ | ✓ | ✓ |
HarRecording | partial | ✓ | ✓ | ✓ |
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.
Provider selection
Section titled “Provider selection”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 });Pool tuning
Section titled “Pool tuning”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).AcquirePageAsyncsurfaces aTimeoutExceptionpast this window.
Sandboxing untrusted content
Section titled “Sandboxing untrusted content”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.
Observability
Section titled “Observability”- Meter
Granit.Browsing— countersgranit.browsing.pages.acquired.count,granit.browsing.pages.released.count,granit.browsing.errors.count; histogramsgranit.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 followsbrowsing.{operation}(browsing.page.acquire,browsing.pdf.render, …).
Both are auto-registered by Granit.Observability through
GranitActivitySourceRegistry.
First-run browser provisioning
Section titled “First-run browser provisioning”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:
# DockerfileRUN 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{ "Browsing": { "PuppeteerSharp": { "ChromiumExecutablePath": "/usr/bin/chromium", "SkipChromiumDownload": true, "DisableSandbox": true // common in Docker containers without user namespaces } }}Related
Section titled “Related”Granit.BlobStorage— same abstraction-then-provider pattern for binary storage.Granit.Imaging— same pattern for image processing.Granit.Documents.Renditions— consumesIPdfViewerCapabilityto rasterise PDF uploads into thumbnails through the rendition pipeline.