Handling Proxies and Timeouts in Plugin Requests
Make plugin requests work on managed networks: what the QGIS network stack applies for you, setting transfer timeouts, reading the user configured timeout,…
TL;DR: make every request through the QGIS network stack so the user’s proxy, exclusion list and certificates apply automatically, and set an explicit transfer timeout on each request so a stalled connection fails in seconds rather than minutes. This page is part of the network requests and remote data in plugins guide.
Complete Runnable Code
"""Requests that inherit the user's network configuration and never hang."""
from qgis.core import QgsBlockingNetworkRequest, QgsNetworkAccessManager, QgsSettings
from qgis.PyQt.QtCore import QUrl
from qgis.PyQt.QtNetwork import QNetworkRequest
DEFAULT_TIMEOUT_MS = 15000
def configured_timeout_ms() -> int:
"""The user's own network timeout from the QGIS options, with a fallback."""
value = QgsSettings().value("qgis/networkAndProxy/networkTimeout", 0, type=int)
return value if value > 0 else DEFAULT_TIMEOUT_MS
def build_request(url: str, timeout_ms: int | None = None) -> QNetworkRequest:
"""A request that will time out rather than hang, with a useful user agent."""
request = QNetworkRequest(QUrl(url))
request.setRawHeader(b"User-Agent", b"MyQgisPlugin/1.0 (+https://example.org/myplugin)")
request.setTransferTimeout(timeout_ms or configured_timeout_ms())
request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
return request
def proxy_summary() -> str:
"""Describe the proxy QGIS would use, for a diagnostics panel or a log line."""
proxy = QgsNetworkAccessManager.instance().proxy()
if proxy.type() == proxy.NoProxy:
return "no proxy"
return "%s:%s" % (proxy.hostName(), proxy.port())
def fetch(url: str) -> bytes:
"""A blocking fetch for use inside a task. Raises on any failure."""
blocking = QgsBlockingNetworkRequest()
error = blocking.get(build_request(url), forceRefresh=False)
if error != QgsBlockingNetworkRequest.NoError:
raise ConnectionError("%s: %s" % (url, blocking.errorMessage()))
return bytes(blocking.reply().content())
Architecture Breakdown
What the stack applies for you
QgsNetworkAccessManager is a QNetworkAccessManager that QGIS has already configured. By the time your request leaves it, the proxy from the options dialog has been applied, hosts on the user’s exclusion list have been routed around it, any client certificate in the authentication database is attached, and the disk cache has been consulted.
None of that is available to a plugin that uses requests or urllib. The code works on a laptop with a direct connection and fails on every managed desktop in an organisation, with an error the user cannot act on because the settings they configured are being ignored.
Transfer timeouts
setTransferTimeout() aborts a request that makes no progress for the given interval. Without it, the effective timeout is whatever the operating system’s TCP stack decides, which on some platforms is measured in minutes.
The four timeouts in that table stack rather than compete, and only one of them is under your control. Reading the user’s configured network timeout from settings, as the example does, means a user who has already told QGIS to be patient does not have to tell your plugin separately.
The exclusion list
QGIS lets a user list hosts that should bypass the proxy — typically internal services. That list lives in the network settings and is applied by the access manager, which means a plugin using the stack gets it for free and a plugin bypassing the stack sends internal traffic to an external proxy that will refuse it.
This is the most common shape of “your plugin works for me but not for my colleague”, because the two people are on different networks with different exclusion lists.
Diagnosing an Environment-Specific Failure
The reports that begin “it works on my machine” are, in networking, almost always literally true — and the differences between the two machines fall into a short list.
When a request fails on one machine and not another, the difference is almost always configuration rather than code. Three checks resolve most cases quickly.
First, confirm the request is actually going through the QGIS stack — a plugin that imported requests at some point in its history may still have a call site that does. Second, look at whether the failure is a TLS error, which usually means an internal certificate authority that is in the system trust store on one machine and not the other. Third, ask whether the host is internal: if so, it probably belongs on the proxy exclusion list, and the fix is a settings change rather than a code change.
Logging proxy_summary() alongside the failing URL turns that three-step diagnosis into one line in a support ticket.
Adding a Diagnostics Action
def network_diagnostics(iface, probe_url: str) -> None:
"""Report what the plugin can reach, for a support conversation."""
lines = ["proxy: %s" % proxy_summary(),
"timeout: %d ms" % configured_timeout_ms()]
try:
payload = fetch(probe_url)
lines.append("probe: ok (%d bytes)" % len(payload))
except ConnectionError as exc:
lines.append("probe: FAILED — %s" % exc)
iface.messageBar().pushInfo("My Plugin", " | ".join(lines))
A plugin that can describe its own network situation saves a great deal of back-and-forth, and the whole thing is a dozen lines.
Why This Is Worth Getting Right Once
Network configuration is the area where a plugin is most likely to work perfectly for its author and fail for a large fraction of its users, because the author’s network is usually the simplest one any user will have. A home or small-office connection has no proxy, no interception, no internal certificate authority and no exclusion list, and none of the code paths that deal with those get exercised.
The remedy is not to write more networking code but to write less of it: every one of those concerns is already handled by the stack the application ships. A plugin whose entire networking surface is “build a request, hand it to the QGIS manager, set a timeout” inherits correct behaviour on networks its author has never seen.
That is also why the diagnostics helper above earns its place. When something does go wrong on a network you cannot reach, a single line reporting the proxy, the timeout and the outcome of a probe request will usually identify the cause without a second exchange.
Production Best Practices
- Always route through
QgsNetworkAccessManagerorQgsBlockingNetworkRequest. - Set a transfer timeout on every request. The default is effectively none.
- Read the user’s configured timeout rather than inventing your own number.
- Log the proxy and the URL together when a request fails; separately they say little.
- Do not implement proxy handling yourself. It is already done, and doing it twice produces requests that honour the wrong configuration.
- Test on a machine behind a proxy at least once before release, or the first person to try it will be a user.
Frequently Asked Questions
How do I know whether a proxy is in use?
QgsNetworkAccessManager.instance().proxy() returns the effective proxy for the session, and its type is NoProxy when none applies. This is worth surfacing in any diagnostics output, because the difference between “no proxy configured” and “a proxy that is refusing the connection” is invisible from the error message alone.
Can I override the proxy for one request?
You can set a proxy on an individual request, and you almost never should. The user configured the proxy for a reason, frequently one imposed by their organisation, and a plugin that routes around it is both surprising and, on some networks, a policy violation. If a specific host must bypass the proxy, the correct place to say so is the exclusion list.
Why does the first request take so long and the rest are fast?
Usually DNS and TLS handshaking, both of which are cached after the first connection. A related cause is proxy authentication: the first request through an authenticating proxy may involve a challenge-response round trip that later requests on the same connection skip.
If the first request is slow enough to matter, warming the connection at plugin load is occasionally justified — but only if the plugin is certain to make requests, since otherwise it is network traffic nobody asked for.
What timeout should I use for a large download?
setTransferTimeout measures inactivity, not total duration, so a long download that keeps making progress is not affected by a fifteen-second timeout. That makes a single moderate value correct for both small API calls and large file transfers, which is a good reason to prefer it over a total-duration deadline.
Does the QGIS cache apply to my requests?
Yes, when the service sends cache headers. QgsBlockingNetworkRequest.get() takes a forceRefresh argument that bypasses it when you need current data. For an endpoint that changes rarely — a capabilities document, a reference list — leaving the cache in play is a meaningful saving for both you and the service operator.
What if the service uses a self-signed certificate?
The user can add the certificate authority to the QGIS authentication database, after which the stack trusts it for every request. Do not disable certificate verification in code as a workaround: it turns a configuration problem into a security one, and it will be copied into other plugins by whoever reads your source.
Related
- Network Requests and Remote Data in Plugins — the parent guide covering the request APIs
- Fetching JSON from an API Without Freezing QGIS — the asynchronous request these settings apply to
- Storing Credentials with QgsAuthManager — where a client certificate lives