native_proxy_resolver 0.1.0
native_proxy_resolver: ^0.1.0 copied to clipboard
Resolve the OS proxy per URL, including PAC and WPAD auto-config, and plug it into dart:io HttpClient, package:http and Dio. Android, iOS, macOS, Windows, Linux.
native_proxy_resolver #
Resolve the operating system's proxy for each URL — including PAC
scripts and WPAD auto-discovery used on corporate networks — and plug the
answer into dart:io HttpClient, package:http and Dio.
Why #
Dart's HttpClient ignores the OS proxy settings
(flutter/flutter#26359).
Existing packages only read one static host:port (system_proxy is
unmaintained since 2021, native_flutter_proxy is static only). In a company
network the proxy is usually chosen per URL by a PAC script, so a static value
is wrong for half the hosts. This plugin asks the OS resolver itself, so your
app routes exactly like the platform's native networking stack.
Install #
dependencies:
native_proxy_resolver: ^0.1.0
Usage #
Resolve a URL #
import 'package:native_proxy_resolver/native_proxy_resolver.dart';
final proxies = await SystemProxy.resolve(Uri.parse('https://example.com'));
// [PROXY proxy.corp:8080, DIRECT] or [DIRECT]
final detail = await SystemProxy.resolveDetailed(Uri.parse('https://example.com'));
print(detail.source); // pac, autoDetect, manual, environment, none
print(detail.pacUrl); // http://wpad.corp/wpad.dat
print(detail.error); // e.g. "PAC evaluation timed out" (entries fall back to DIRECT)
print(detail.toFindProxyString()); // "PROXY proxy.corp:8080; DIRECT"
Resolution never throws for resolver failures: you get [DIRECT] plus
error. Results are cached per origin (scheme://host:port, TTL 5 min,
failures 30 s, 256 origins LRU) and concurrent lookups share one native call.
Tune it with your own resolver:
SystemProxy.resolver = SystemProxyResolver(
cacheTtl: const Duration(minutes: 2),
timeout: const Duration(seconds: 5),
);
SystemProxy.onChange.listen((_) => print('network or proxy changed'));
dart:io HttpClient #
HttpClient.findProxy is a synchronous callback, but asking the OS (and
running a PAC script) is asynchronous. ProxyAwareHttpClient solves this with
pre-resolution: every getUrl/openUrl/post/… first awaits the proxy
decision for the URL's origin, then opens the request on the wrapped client,
whose findProxy reads the now-warm cache.
final client = ProxyAwareHttpClient(); // wraps HttpClient()
final response = await (await client.getUrl(url)).close();
Or make every HttpClient() in the app proxy-aware (covers SDKs,
NetworkImage, package:http's default client on the VM…):
void main() {
SystemProxy.installHttpOverrides();
runApp(const MyApp());
}
Trade-offs you should know:
| Strategy | How | Cost |
|---|---|---|
| Pre-resolve (default) | ProxyAwareHttpClient() |
First request to an origin waits for the OS (≈ instant for static config; PAC download/WPAD bounded by timeout). |
| Warmed cache | ProxyAwareHttpClient(preResolve: false) + SystemProxy.warmUp([...]) |
No waiting; a not-yet-warmed origin uses fallback (DIRECT) once and is resolved in the background. |
| Plain callback | client.findProxy = SystemProxy.resolver.findProxyCallback() |
Same as warmed cache, for a client you already own. |
Redirects are followed inside dart:io and only hit the synchronous callback;
a redirect to a never-seen origin uses fallback for that hop. Set
request.followRedirects = false if every hop must use its PAC decision.
package:http #
import 'package:native_proxy_resolver/http.dart';
final client = createSystemProxyHttpClient();
final response = await client.get(Uri.parse('https://example.com'));
client.close();
Dio #
The package does not depend on Dio. Copy
example/lib/dio_integration.dart:
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:native_proxy_resolver/native_proxy_resolver.dart';
Dio createSystemProxyDio({SystemProxyResolver? resolver}) {
final dio = Dio();
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () => ProxyAwareHttpClient(resolver: resolver),
);
return dio;
}
Helpers #
ProxyEntry.toFindProxyString(entries)—PROXY host:port; DIRECTsyntax.ProxyBypassRules.parse(spec, style: BypassStyle.noProxy | wildcard)— curl-styleno_proxy(suffixes,*, IPs, CIDR, ports) and Windows / Android / GNOME exclusion lists (<local>, globs, CIDR).EnvironmentProxyConfig.fromEnvironment(Platform.environment).ProxyListParser.parsePacResult/parseServerList/parseProxyUrl.
Platform support #
| Platform | Mechanism | PAC | WPAD | Change events |
|---|---|---|---|---|
| Android 7+ (API 24) | ProxySelector.select(URI) + ConnectivityManager.getDefaultProxy() |
Yes — via Android's local PAC proxy (localhost:<port>) |
Where the OS supports it | Yes |
| iOS 13+ | CFNetworkCopyProxiesForURL; PAC run with CFNetworkExecuteProxyAutoConfigurationURL/Script on a background run loop with timeout |
Yes | Yes | Network path changes |
| macOS 10.15+ | same as iOS | Yes | Yes | Yes (SCDynamicStore proxies key + path) |
| Windows 10+ | WinHttpGetIEProxyConfigForCurrentUser + WinHttpGetProxyForUrl (auto-detect DHCP/DNS, PAC URL, auto-logon retry), worker thread |
Yes | Yes | No — cache TTL |
| Linux | http_proxy/https_proxy/all_proxy/no_proxy, else GNOME gsettings |
No (reported) | No | GNOME (gsettings monitor) |
| Web | not supported (the browser applies its own proxy) |
macOS apps need the com.apple.security.network.client entitlement to
download PAC scripts from inside the sandbox. Android adds
ACCESS_NETWORK_STATE to your manifest.
Limitations #
- Linux has no PAC engine: a GNOME
autoconfiguration resolves toDIRECTwithsource: pac,pacUrland an explanatoryerror. - Android hides the individual PAC decision behind its local PAC proxy,
so PAC networks report
PROXY localhost:<port>. dart:iocan only use HTTP proxies. SOCKS andHTTPS(TLS-to-proxy) entries are returned byresolvebut skipped infindProxystrings.- The cache key is the origin; PAC rules that branch on the URL path are re-evaluated only when the entry expires.
- Proxy passwords held in the Keychain / Windows credential store are not
read; use
HttpClient.addProxyCredentials/authenticateProxy. - Windows has no change notification (entries expire after the TTL).
- Uses platform channels: call from the root isolate, or run
BackgroundIsolateBinaryMessenger.ensureInitializedfirst.
Example #
example/ resolves any URL, shows source/PAC/errors, fetches it
through ProxyAwareHttpClient, package:http, Dio and a plain HttpClient()
with the global overrides toggled, and lists change events.
