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";
}
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
- Replace the example names and networks with your own, and remove the blocks you do not need.
- Open it in the PAC file checker with the URLs that matter to you, and fix every finding above “info”.
- Serve it with the content type
application/x-ns-proxy-autoconfigand 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.