# PAC and wpad.dat best practices

Source: https://findproxyforurl.net/best-practices/ · updated 2026-10-12

Changed 2026-10-12: console now has its own rule, because it works in Chrome and current pactester builds but not on Windows or in older pactester builds; the earlier statement that Chrome does not define console was wrong. The new rule for window, navigator, fetch, setTimeout, process, require and similar objects now counts in the rating, because no engine defines them.

A PAC file decides, for every web request, whether the browser uses a proxy. These are the habits that keep such a file fast, predictable in every browser and safe when something breaks. Each point links to the rule in the catalogue that checks it.

This page lists what to do. The opposite view, the mistakes that most often break routing, is on
the page with the [20 most common PAC and wpad.dat mistakes](https://findproxyforurl.net/top-20/). If you are new to PAC files,
start with [what a PAC file is](https://findproxyforurl.net/articles/what-is-a-pac-file/).

## The checklist in short

| Do this | Why | Checked by |
|---|---|---|
| Write ES5 JavaScript only | Some PAC engines reject newer syntax and then send everything direct | [PAC-K001](https://findproxyforurl.net/rules/es2015-syntax/), [PAC-K010](https://findproxyforurl.net/rules/es2015-builtins/) |
| Make no DNS lookups for names | A lookup on every request is slow and fails open | [PAC-P001](https://findproxyforurl.net/rules/dns-function-on-hot-path/), [PAC-P002](https://findproxyforurl.net/rules/isinnet-on-hostname/) |
| Lower-case the host once | Not every engine lower-cases it for you | [PAC-K009](https://findproxyforurl.net/rules/host-not-lowercased/), [PAC-C018](https://findproxyforurl.net/rules/uppercase-pattern-literal/) |
| Write IP addresses in plain dotted form | `010.1.2.3`, hex or one-number forms are read differently by engines and by people | [PAC-C031](https://findproxyforurl.net/rules/ip-address-unusual-form/) |
| Put cheap tests before expensive ones | Most requests are decided by the first rules | [PAC-P005](https://findproxyforurl.net/rules/expensive-before-cheap/) |
| Match domains with a leading dot | Without it, look-alike domains match too | [PAC-X003](https://findproxyforurl.net/rules/dnsdomainis-without-leading-dot/), [PAC-X002](https://findproxyforurl.net/rules/shexpmatch-trailing-asterisk/) |
| End with a default route and a second proxy | A missing or `DIRECT` fallback opens the network silently | [PAC-X001](https://findproxyforurl.net/rules/no-default-return/), [PAC-B011](https://findproxyforurl.net/rules/no-proxy-failover/), [PAC-X014](https://findproxyforurl.net/rules/direct-fallback-in-proxy-list/) |
| Serve it with the right type, a cache lifetime and HTTPS | Clients then get the file safely and changes arrive on time | [PAC-D001](https://findproxyforurl.net/rules/pac-over-plain-http/), [PAC-D002](https://findproxyforurl.net/rules/wrong-content-type/), [PAC-D003](https://findproxyforurl.net/rules/missing-cache-control/) |
| Version and comment the file | The next person can tell what changed and what is intended | [PAC-B004](https://findproxyforurl.net/rules/undocumented-default-route/) |
| Test it automatically | A wrong route is invisible until someone complains | [testing in CI](https://findproxyforurl.net/articles/test-pac-files-in-ci-with-pactester/) |

The sections below explain each point. A complete file that follows all of them comes at the end.

## Write the file in ES5, the JavaScript every engine understands

ES5 is the 2009 edition of JavaScript. Browsers today accept much newer code, but PAC files also run
in other engines. Windows runs the file for Windows services, many security agents and programs that
follow the system proxy settings, and in the lab (2026-10-11) it understood JavaScript up to about the
2017 edition: `let`, `const` and arrow functions worked, but `?.`, `??` or object spread made the whole
file a syntax error, and those programs then connect directly
([PAC-K020](https://findproxyforurl.net/rules/es2018-syntax/)). Older engines such as pactester 1.4 fail even on `let`, `const`,
an arrow function or a template string ([PAC-K001](https://findproxyforurl.net/rules/es2015-syntax/)). Newer library methods
such as `startsWith` or `flat` fail in the same way at run time where an engine lacks them
([PAC-K010](https://findproxyforurl.net/rules/es2015-builtins/)).

- Use `var`, `function` and `indexOf`; nothing newer.
- Keep the file to plain ASCII characters. Typographic quotes copied from a word processor are a
  syntax error ([PAC-E014](https://findproxyforurl.net/rules/non-ascii-characters/)). Save it as UTF-8 or ASCII: a file saved
  as UTF-16, as Notepad's "Unicode" setting does, does not load on Windows
  ([PAC-E013](https://findproxyforurl.net/rules/byte-order-mark/)).
- Leave out `alert()` and `console.log()` in production ([PAC-K002](https://findproxyforurl.net/rules/alert-in-pac/),
  [PAC-K022](https://findproxyforurl.net/rules/console-in-pac/)). Objects from web pages or Node.js, such as `window`,
  `navigator` or `process`, do not exist in a PAC file at all
  ([PAC-E018](https://findproxyforurl.net/rules/browser-or-node-objects-in-pac/)).

## Never let a rule depend on DNS

[`isInNet`](https://findproxyforurl.net/functions/isinnet/), [`dnsResolve`](https://findproxyforurl.net/functions/dnsresolve/) and
[`isResolvable`](https://findproxyforurl.net/functions/isresolvable/) look up the host name in DNS when they are given a name.
The browser waits for that lookup on every request, and when DNS is slow or fails, the rule quietly
takes a different branch, usually the direct one. The [article on DNS in PAC files](https://findproxyforurl.net/articles/dns-in-pac-files/)
shows the details for each engine.

- Decide by the name first, with [`dnsDomainIs`](https://findproxyforurl.net/functions/dnsdomainis/) and
  [`shExpMatch`](https://findproxyforurl.net/functions/shexpmatch/).
- Call `isInNet` only when the host already is an IP address. A simple pattern test such as
  `/^\d+\.\d+\.\d+\.\d+$/.test(host)` in front of it is enough, and so is the text prefix of the
  range, as in `shExpMatch(host, "10.*") && isInNet(host, "10.0.0.0", "255.0.0.0")`. The page of
  [PAC-P002](https://findproxyforurl.net/rules/isinnet-on-hostname/) lists every form of this test that the checker accepts.
- Do not use `isResolvable` as a test of whether the network is reachable
  ([PAC-P003](https://findproxyforurl.net/rules/isresolvable-usage/)), and do not route by
  [`myIpAddress()`](https://findproxyforurl.net/functions/myipaddress/), which returns an unreliable address on machines with
  several network adapters or a VPN ([PAC-K013](https://findproxyforurl.net/rules/myipaddress-usage/)).

## Lower-case the host name before you compare it

Chromium and Firefox pass the host name to the PAC in lower case, but WinHTTP is reported to pass it
as the user typed it. One line at the top of the function makes the file behave the same everywhere:
`host = host.toLowerCase();` ([PAC-K009](https://findproxyforurl.net/rules/host-not-lowercased/)). Write every pattern in lower
case as well ([PAC-C018](https://findproxyforurl.net/rules/uppercase-pattern-literal/)).

## Write IP addresses as four plain decimal numbers

Write every IPv4 address as four decimal numbers from 0 to 255 without leading zeros, such as
`10.1.2.3`. Other forms are legal in many programs: a leading zero makes a part octal, so `010` means
8, `0x0a` is hex, and `10.1.2` or `167838211` also stand for an address. The PAC engines do not read
these forms the same way. Chrome, Edge and Firefox turn the requested host into the plain form
before the file sees it, so `host == "010.1.2.3"` never matches there, while pactester compares the
text as written. In `isInNet()` the browsers and pactester read a leading zero as decimal, but
Windows reads it as octal.

Even where such a form happens to work in every engine you use, it is bad practice. The next person
reads `010.1.2.3` as 10.1.2.3, copies it into another rule or a firewall object, and gets a different
address. Plain addresses can be searched for and compared; unusual forms hide mistakes. The same
goes for IPv6: in a test on `host`, write the short lower-case form without brackets
(`2001:db8::a`), because that is what the browsers pass. Brackets belong only in a proxy entry such as
`PROXY [2001:db8::a]:8080` ([PAC-C031](https://findproxyforurl.net/rules/ip-address-unusual-form/)).

## Put the cheapest tests first

The engine runs the rules from top to bottom and stops at the first `return`. Plain string
comparisons cost almost nothing; anything that might touch DNS costs a lookup. Put the string rules
first, so that most requests are decided before an expensive rule runs
([PAC-P005](https://findproxyforurl.net/rules/expensive-before-cheap/)). For the same reason, a file with hundreds of rules or
deep nesting is slow and hard to review ([PAC-P006](https://findproxyforurl.net/rules/large-pac-file/),
[PAC-P007](https://findproxyforurl.net/rules/high-complexity/)).

## Match domains with a leading dot, and list short names explicitly

- `dnsDomainIs(host, ".corp.example")` matches `www.corp.example` but not `evilcorp.example`. Without
  the leading dot, look-alike domains match too ([PAC-X003](https://findproxyforurl.net/rules/dnsdomainis-without-leading-dot/)).
- With `shExpMatch`, write `*.corp.example`, never `*corp.example` or `corp.*`
  ([PAC-X004](https://findproxyforurl.net/rules/shexpmatch-leading-asterisk-without-dot/),
  [PAC-X002](https://findproxyforurl.net/rules/shexpmatch-trailing-asterisk/)).
- Match on `host`, not on `url`; the URL also contains the path and the query, which the user or a
  web page controls ([PAC-C001](https://findproxyforurl.net/rules/url-instead-of-host/)).
- A "plain host name" is a name without a dot, such as `intranet`. [`isPlainHostName`](https://findproxyforurl.net/functions/isplainhostname/)
  is true for every such name, including a mistyped public one. If you want typos to reach the proxy
  and its logs, list your real short names instead ([PAC-B006](https://findproxyforurl.net/rules/plain-hostname-blanket-direct/)).
- Match IP ranges with `isInNet` on IP addresses, not with text prefixes such as `10.*`
  ([PAC-X006](https://findproxyforurl.net/rules/rfc1918-string-pattern/)).

## Always end with a default route that does not fail open

"Fail open" means that an error lets traffic out without the proxy. A PAC file fails open in three
common ways, and each has a simple fix:

1. **No default route.** If no rule matches and the function ends without a `return`, the browser
   connects directly. End the function with an unconditional `return`
   ([PAC-X001](https://findproxyforurl.net/rules/no-default-return/)).
2. **`DIRECT` as the last fallback.** `"PROXY p1:8080; DIRECT"` sends everything directly while the
   proxy is down. Use a second proxy instead: `"PROXY p1:8080; PROXY p2:8080"`
   ([PAC-B011](https://findproxyforurl.net/rules/no-proxy-failover/), [PAC-X014](https://findproxyforurl.net/rules/direct-fallback-in-proxy-list/)). If a
   direct fallback is intended, for example on roaming laptops, say so in a comment.
3. **A `try`/`catch` that returns `DIRECT`.** It turns every error into a bypass. Leave the catch
   out, or return the proxy from it ([PAC-X011](https://findproxyforurl.net/rules/fail-open-catch-direct/)).

The PAC file is not a security control on its own. A client can ignore it, so the firewall should
only let clients reach the internet through the proxy. Then a broken PAC file shows up as an error
instead of as uncontrolled access.

## Serve the file with the right type, a cache lifetime and over HTTPS

How the file reaches the client matters as much as what it says.

- **MIME type:** send `Content-Type: application/x-ns-proxy-autoconfig`
  ([PAC-D002](https://findproxyforurl.net/rules/wrong-content-type/)). Some clients accept other types, but a `text/html`
  answer usually means the server returned an error page.
- **Cache lifetime:** send `Cache-Control: max-age=3600` or any value up to one day
  ([PAC-D003](https://findproxyforurl.net/rules/missing-cache-control/)). Without it, each browser decides on its own when to
  fetch the file again. `no-cache` makes clients fetch it far too often
  ([PAC-D004](https://findproxyforurl.net/rules/cache-control-no-cache/)), and more than a day lets a mistake live too long
  ([PAC-D005](https://findproxyforurl.net/rules/cache-control-too-long/)).
- **HTTPS for an explicit URL:** if you configure the PAC URL by group policy or MDM, use an
  `https://` URL. Over plain HTTP, anyone on the network path can rewrite the file and send all
  traffic to a proxy of their choice ([PAC-D001](https://findproxyforurl.net/rules/pac-over-plain-http/)).
- **WPAD stays plain HTTP:** automatic discovery through DNS always asks for
  `http://wpad/wpad.dat`, so this delivery cannot be made HTTPS. That is one reason to prefer an
  explicit URL. The [WPAD page](https://findproxyforurl.net/wpad/) explains the protocol and how to make it safer, and the page on
  [automatic proxy detection](https://findproxyforurl.net/wpad/auto-detect/) shows how to switch it off where you do not need it.
- **Status code and size:** the URL must return `200` ([PAC-D010](https://findproxyforurl.net/rules/http-error-status/)); a
  large file should be served compressed ([PAC-D006](https://findproxyforurl.net/rules/uncompressed-large-pac/)). Do not load the
  file from a file share ([PAC-D009](https://findproxyforurl.net/rules/pac-from-file-share/)).

The [PAC file tester](https://findproxyforurl.net/check/) grades the delivery when you paste the output of
`curl -sI https://pac.corp.example/proxy.pac` next to the file.

## Version the file and comment what is intended

- Put a version and an owner in the first comment lines, and keep a short change log there or in
  version control. When a user reports a problem, you can then ask which version their browser has.
- Comment the default route; it decides every request that no rule matched
  ([PAC-B004](https://findproxyforurl.net/rules/undocumented-default-route/)).
- Keep the file in version control and change it through review, like code.
- Do not repeat the same proxy string twenty times. Keep it in one place, so a change of proxy is
  one edit ([PAC-B005](https://findproxyforurl.net/rules/repeated-proxy-string-literal/)).

## Test the file automatically before every change

Write down the URLs that matter and the answer you expect for each: an internal site, an internal
IP address, a public site, a plain host name, and anything with a special rule. Then let a script
compare the real answers with the expected ones before every release. The article on
[testing PAC files in CI with pactester](https://findproxyforurl.net/articles/test-pac-files-in-ci-with-pactester/) has a
copyable script. Before you publish, also run the file in the [PAC file tester](https://findproxyforurl.net/check/). It
rates the file, and its live trace shows step by step how one URL is routed. Check the important
URLs once in each engine profile your users run, because Chromium, Firefox and pacparser can
disagree ([PAC-K012](https://findproxyforurl.net/rules/engine-divergent-result/)). The article on
[how to test a PAC file](https://findproxyforurl.net/articles/how-to-test-a-pac-file/) explains which URLs to choose.

## A complete file that follows every point

This file uses only synthetic names. It sends internal short names, the internal domain and local
addresses direct, and everything else to two proxies. It gets an A+ in the tester.

```js
// proxy.pac for corp.example
// Version 2026-10-09.1, owner: network team, tests: pac-tests.txt next to this file
// Change log: 2026-10-09.1 added the IPv6 local ranges.

// Internal short names that people type without a domain.
var shortNames = ["intranet", "wiki", "print"];

function FindProxyForURL(url, host) {
  // Compare host names in lower case only.
  host = host.toLowerCase();

  // 1. Known internal short names and the internal domain go direct.
  if (shortNames.indexOf(host) >= 0 || dnsDomainIs(host, ".corp.example")) {
    return "DIRECT";
  }

  // 2. IPv6 loopback and local addresses go direct.
  if (host == "::1" || shExpMatch(host, "fe80:*") || shExpMatch(host, "fd*:*")) {
    return "DIRECT";
  }

  // 3. Private and loopback IPv4 addresses go direct. isInNet only runs when
  //    the host already is an IP address, so it never causes a DNS lookup.
  if (/^\d+\.\d+\.\d+\.\d+$/.test(host) &&
      (isInNet(host, "10.0.0.0", "255.0.0.0") ||
       isInNet(host, "172.16.0.0", "255.240.0.0") ||
       isInNet(host, "192.168.0.0", "255.255.0.0") ||
       isInNet(host, "127.0.0.0", "255.0.0.0"))) {
    return "DIRECT";
  }

  // Default route: everything else goes to the proxy; the second proxy is the
  // fallback. No DIRECT at the end, so a proxy outage does not open the network.
  return "PROXY proxy1.corp.example:8080; PROXY proxy2.corp.example:8080";
}
```

Copy it into the [PAC file tester](https://findproxyforurl.net/check/) to see the result for your own URLs, or start a new
file in the [PAC file generator](https://findproxyforurl.net/generate/).

## What not to do

The [20 most common PAC and wpad.dat mistakes](https://findproxyforurl.net/top-20/) are the other side of this checklist,
ranked by how often they appear and how much harm they do. The full [rule catalogue](https://findproxyforurl.net/rules/) lists
every check the tester makes.


## Frequently asked questions

### What are the most important PAC file best practices?

Write plain ES5 JavaScript, avoid DNS lookups, lower-case the host name once at the top, end with an unconditional default route to the proxy, serve the file over HTTPS with the MIME type application/x-ns-proxy-autoconfig and a cache lifetime of at most a day, and test it automatically before every change.

### Are the best practices for wpad.dat different from those for proxy.pac?

The file itself follows the same rules, because a wpad.dat is a PAC file. Only the delivery differs. WPAD discovery fetches the file over plain HTTP from a name that anyone on the network path may be able to answer, so own every wpad name in your DNS suffixes or, better, configure the PAC URL explicitly.

### Should a PAC file end with DIRECT as a fallback?

Only if uninspected traffic during a proxy outage is acceptable. A second proxy as fallback keeps traffic inspected. If you do keep DIRECT, say so in a comment so the next person knows it is intended.

### May I write an IP address with leading zeros or in hex in a PAC file?

Do not. 010.1.2.3, 0x0a.1.2.3 and 167838211 are read differently by the browsers, by pactester and by Windows; a leading zero can mean octal, so 010 is 8. Even where it works, the form is confusing and leads to mistakes. Write four plain decimal numbers such as 10.1.2.3.

### May I use isInNet in a PAC file?

Yes, but only on host names that already are IP addresses. Called with a name, isInNet makes a DNS lookup for every request, which slows browsing and makes the result depend on DNS.

### How do I check a PAC file against these best practices?

Paste it into the PAC file tester at findproxyforurl.net/check/. It runs the file in the Chromium, Firefox or pacparser engine profile you choose and grades it against the public rule catalogue; a file that follows every point on this page gets an A+.

