PAC and wpad.dat best practices

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. If you are new to PAC files, start with what a PAC file is.

The checklist in short

Do thisWhyChecked by
Write ES5 JavaScript onlySome PAC engines reject newer syntax and then send everything directPAC-K001, PAC-K010
Make no DNS lookups for namesA lookup on every request is slow and fails openPAC-P001, PAC-P002
Lower-case the host onceNot every engine lower-cases it for youPAC-K009, PAC-C018
Write IP addresses in plain dotted form010.1.2.3, hex or one-number forms are read differently by engines and by peoplePAC-C031
Put cheap tests before expensive onesMost requests are decided by the first rulesPAC-P005
Match domains with a leading dotWithout it, look-alike domains match tooPAC-X003, PAC-X002
End with a default route and a second proxyA missing or DIRECT fallback opens the network silentlyPAC-X001, PAC-B011, PAC-X014
Serve it with the right type, a cache lifetime and HTTPSClients then get the file safely and changes arrive on timePAC-D001, PAC-D002, PAC-D003
Version and comment the fileThe next person can tell what changed and what is intendedPAC-B004
Test it automaticallyA wrong route is invisible until someone complainstesting in CI

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). Older engines such as pactester 1.4 fail even on let, const, an arrow function or a template string (PAC-K001). Newer library methods such as startsWith or flat fail in the same way at run time where an engine lacks them (PAC-K010).

  • 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). 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).
  • Leave out alert() and console.log() in production (PAC-K002, PAC-K022). Objects from web pages or Node.js, such as window, navigator or process, do not exist in a PAC file at all (PAC-E018).

Never let a rule depend on DNS

isInNet, dnsResolve and 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 shows the details for each engine.

  • Decide by the name first, with dnsDomainIs and 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 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), and do not route by myIpAddress(), which returns an unreliable address on machines with several network adapters or a VPN (PAC-K013).

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). Write every pattern in lower case as well (PAC-C018).

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).

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). For the same reason, a file with hundreds of rules or deep nesting is slow and hard to review (PAC-P006, PAC-P007).

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).
  • With shExpMatch, write *.corp.example, never *corp.example or corp.* (PAC-X004, PAC-X002).
  • 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).
  • A “plain host name” is a name without a dot, such as intranet. 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).
  • Match IP ranges with isInNet on IP addresses, not with text prefixes such as 10.* (PAC-X006).

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).
  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, PAC-X014). 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).

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). 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). 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), and more than a day lets a mistake live too long (PAC-D005).
  • 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).
  • 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 explains the protocol and how to make it safer, and the page on automatic proxy detection shows how to switch it off where you do not need it.
  • Status code and size: the URL must return 200 (PAC-D010); a large file should be served compressed (PAC-D006). Do not load the file from a file share (PAC-D009).

The PAC file tester 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).
  • 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).

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 has a copyable script. Before you publish, also run the file in the PAC file tester. 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). The article on 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.

// 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 to see the result for your own URLs, or start a new file in the PAC file generator.

What not to do

The 20 most common PAC and wpad.dat mistakes are the other side of this checklist, ranked by how often they appear and how much harm they do. The full rule catalogue 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+.