A reference proxy.pac, explained block by block

This is the kind of PAC file a company network can run in production. It is shown in full first, then each block is explained, with the reason it sits where it does.

The whole file

// proxy.pac - reference file for Example Corp (synthetic)
// Owner: network team, change NET-0001. Served as https://pac.corp.example/proxy.pac
// Test every change at https://findproxyforurl.net/check/ before it goes live.
// Annotated at https://findproxyforurl.net/examples/reference/

function FindProxyForURL(url, host) {
  // host names are case-insensitive, string comparisons are not
  host = host.toLowerCase();

  // 1. plain host names (no dot) are on the local network
  if (isPlainHostName(host)) {
    return "DIRECT";
  }

  // 2. internal DNS zones: the zone itself and every host in it
  if (host == "corp.example" || dnsDomainIs(host, ".corp.example") ||
      host == "corp-dev.example" || dnsDomainIs(host, ".corp-dev.example")) {
    return "DIRECT";
  }

  // 3. single exceptions, each with its reason
  if (host == "hr.partner.example" ||  // HR portal, reached over the site-to-site VPN
      host == "updates.example.net") {  // update mirror in the DMZ
    return "DIRECT";
  }

  // 4. destinations that need a different proxy
  if (host == "partner.example" || dnsDomainIs(host, ".partner.example")) {
    return "PROXY partner-gw.corp.example:3128";  // partner VPN gateway
  }

  // 5. IP-address URLs only: isInNet() on a host name would trigger a DNS lookup
  if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
    // private, loopback and link-local addresses
    if (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") ||
        isInNet(host, "169.254.0.0", "255.255.0.0")) {
      return "DIRECT";
    }
    // the DMZ network, which is public address space but ours
    if (isInNet(host, "198.51.100.0", "255.255.255.0")) {
      return "DIRECT";
    }
  }

  // 6. Default route: everything not matched above goes through the corporate proxies.
  // Failover order: proxy1 (data centre 1), then proxy2 (data centre 2).
  // No DIRECT at the end: if both proxies are down, requests fail instead of bypassing the proxy.
  return "PROXY proxy1.corp.example:8080; PROXY proxy2.corp.example:8080";
}

Test this file

Why the file is built this way

A PAC file runs once for every request the browser makes, so it has to be fast, it must not wait for the network, and it must give the same answer in every browser. The reference file meets those three goals with a simple shape: a list of if blocks that each end in a return, ordered from the cheapest test to the most expensive one, and one default route at the end. The first block that matches decides the request, and nothing after it runs.

Each block is explained below in the order in which it runs.

The header says who owns the file and how to test it

// proxy.pac - reference file for Example Corp (synthetic)
// Owner: network team, change NET-0001. Served as https://pac.corp.example/proxy.pac
// Test every change at https://findproxyforurl.net/check/ before it goes live.

Comments cost nothing at run time. The next person who opens the file, often during an outage, needs to know who owns it, which change introduced the current version, and where it is served from. Keep the file plain ASCII: “smart” quotes pasted from a word processor are a syntax error (details).

The host is lower-cased once, at the start

  host = host.toLowerCase();

Host names are not case-sensitive, but the string comparison == is. Chromium and Firefox already pass the host in lower case. WinHTTP, the HTTP service that Windows programs use, is reported to pass it as the user typed it, so WIKI.Corp.Example would not match .corp.example and would go to the proxy. Lower-casing once makes the file behave the same everywhere (details).

Plain host names go direct first, because that test is the cheapest

  if (isPlainHostName(host)) {
    return "DIRECT";
  }

A plain host name has no dot, such as intranet. The client completes it with its DNS search suffix, so it is almost always an internal server. The test only looks at the string, so it costs almost nothing. One side effect: a mistyped public name without a dot also goes direct and fails at the resolver instead of at the proxy (details).

Internal zones need two tests each

  if (host == "corp.example" || dnsDomainIs(host, ".corp.example") ||
      host == "corp-dev.example" || dnsDomainIs(host, ".corp-dev.example")) {
    return "DIRECT";
  }

dnsDomainIs(host, ".corp.example") matches every host inside the zone, but not the zone name itself, so host == "corp.example" covers that. Both use the leading dot on purpose: without it, dnsDomainIs(host, "corp.example") would also match notcorp.example (details).

Single exceptions are listed one by one, each with its reason

  if (host == "hr.partner.example" ||  // HR portal, reached over the site-to-site VPN
      host == "updates.example.net") {  // update mirror in the DMZ
    return "DIRECT";
  }

Exceptions are where PAC files decay: nobody remembers why an entry is there, so nobody dares to remove it. A comment at the end of each line keeps the reason with the entry. Exact host names are compared with ==; they are cheaper and safer than wildcard patterns with shExpMatch().

Destinations with their own proxy come before the default route

  if (host == "partner.example" || dnsDomainIs(host, ".partner.example")) {
    return "PROXY partner-gw.corp.example:3128";  // partner VPN gateway
  }

This block must come after the direct exceptions, because hr.partner.example is inside partner.example and has to be caught first. The checker reports the single proxy here as an information finding: there is no second gateway to fail over to, which is acceptable for a partner link but worth a conscious decision (details).

IP address tests run only when the URL contains an IP address

  if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {

isInNet() compares an IP address with a network. When it gets a host name instead, the browser first resolves the name in DNS, and every request then waits for that lookup. The regular expression is a cheap string test that lets only URLs such as http://10.20.30.40/ into the block (details). Inside it come the private, loopback and link-local ranges, and then the organisation’s own public range, the DMZ:

    if (isInNet(host, "198.51.100.0", "255.255.255.0")) {
      return "DIRECT";
    }

The default route is the last line, commented, with failover

  // 6. Default route: everything not matched above goes through the corporate proxies.
  // Failover order: proxy1 (data centre 1), then proxy2 (data centre 2).
  // No DIRECT at the end: if both proxies are down, requests fail instead of bypassing the proxy.
  return "PROXY proxy1.corp.example:8080; PROXY proxy2.corp.example:8080";

The default route decides every request that nothing above matched, which is usually most of the traffic. It must be the last statement. If the function can end without a return, Chromium, Firefox and WinHTTP all connect directly, so that traffic silently bypasses the proxy (details). Two proxies give the browser somewhere to go when one is down. The comment states the decision not to fall back to DIRECT, so that nobody “fixes” it later by accident (the trade-off).

How to adapt the file

  1. Replace the example names and networks with your own, and remove the blocks you do not need.
  2. Open it in the PAC file checker with the URLs that matter to you, and fix every finding above “info”.
  3. Serve it with the content type application/x-ns-proxy-autoconfig and roll it out to a pilot group first. The PAC file generator explains where to set the URL in Windows, macOS and group policy.

More patterns, each in a short file of its own, are on the PAC file examples page.

Frequently asked questions

What should a production proxy.pac contain?

A header that says who owns the file and how to test it, lower-casing of the host, direct routes for plain host names, internal zones and private addresses, the exceptions with a comment each, and one commented default route with at least two proxies at the end.

In which order should the tests in a PAC file be?

The cheapest and most frequently true tests first, the expensive ones later, and the default route last. Tests that look at IP addresses go inside one block that runs only when the URL contains an IP address, so no host name is ever resolved.

Should a PAC file end with DIRECT?

Not by default. A DIRECT at the end of the proxy list means that every request bypasses the proxy when the proxies are unreachable. If you want that, add it on purpose and make sure the firewall decides what is allowed.