Safari has no blocking webRequest. Here's what to do instead.
You ran the converter, your extension loads in Safari, and your request blocking silently does nothing. No errors. The listeners just never fire with the power you expected. This is not a bug in your port — it’s Safari’s design, and pretending otherwise wastes weeks.
The model change
In Chrome (historically) your JavaScript could sit in the request path: see each request, decide, block or rewrite it. Safari never allowed that — extension code doesn’t get to watch the network. Instead, Safari implements declarativeNetRequest (DNR): you register rules ahead of time — match patterns, resource types, actions like block, redirect, or header modification — and the browser applies them itself. Chrome pushed the same direction with Manifest V3, so if you already built DNR rules for Chrome, a meaningful part of your Safari migration is done. The differences that remain are in rule limits, supported action types, and versioned behavior — which you verify against the Safari versions you support, not against a blog post.
What survives the migration cleanly
- Static blocklists — domain and URL-pattern blocking translates directly into DNR rules.
- Simple redirects — from-pattern-to-URL rules are well supported.
- Most header tweaks — removing or setting specific headers is expressible as DNR actions in modern Safari.
- User-toggled rule sets — enable and disable rulesets dynamically from your UI.
What needs a redesign
- Logic that inspects request or response content. DNR matches on URL, resource type, and headers-level patterns — it will never show your code the payload. Whatever decision you were making from content has to move elsewhere: into a content script, into heuristics expressible as rules, or out of the product.
- Rules computed per-request at runtime.You can update dynamic rules frequently, but not in the request path. Think “recompile the rule set when state changes,” not “decide per request.”
- Counting and telemetry from the request path. If your UI shows “blocked 47 ads on this page,” you lose the direct count. At Pie we rebuilt counters from what the browser exposes plus DOM-level signals — it’s solvable, but it’s a design task, not a port.
- Massive filter lists. Safari enforces rule limits, and they differ by version. Big lists need prioritization, compilation, and pruning strategy — this is where ad-blocker experience earns its keep.
How I approach the migration
- Inventory every
webRequestlistener and classify what each one actually accomplishes for the user — not how it does it. - Sort into: expressible as static rules · expressible as dynamic rules · needs redesign · not portable. Be ruthless about the last bucket early.
- Build the rule compiler — the code that turns your source of truth (filter list, user settings, server config) into DNR rule sets within Safari’s limits.
- Rebuild user-visible feedback (counters, badges) from signals you still have.
- Test per Safari version — behavior genuinely varies.
The honest summary: DNR is less powerful and more predictable. For most extensions the user-facing feature set survives; the engineering underneath is different enough that this is the part of a Safari port that deserves a specialist.
Migrating a real extension's blocking layer?
I did this migration for an ad blocker with two million users. The port assessment maps every one of your webRequest usages to its Safari answer — kept, redesigned, or cut — with a fixed quote for the work.