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

python
"""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())
What the QGIS network stack adds to a request Three bands showing what a request picks up on its way out: your plugin supplies the URL and headers, the QGIS network layer applies the proxy, exclusions, certificates and cache policy, and the operating system makes the connection. Your plugin URL + headers what you control timeout per request QGIS network proxy + exclusions from the options certificates from the auth database disk cache honours headers The system DNS + TCP + TLS and its own timeouts Everything in the middle band is configuration the user already supplied — and exactly what a plugin using its own HTTP client throws away.

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.

Timeouts that apply to one request A grid of four timeouts — connection, transfer, the plugin-level deadline and the service gateway — showing who enforces each and what happens when it fires. enforced by looks like connection the operating system a slow, unhelpful failure transfer Qt, if you set it a clean OperationCanceledError plugin deadline your own timer whatever you choose gateway timeout the service HTTP 504

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.

The request fails only on one machine A decision tree for environment-specific network failures: a proxy the plugin bypassed, a certificate the system does not trust, and a host excluded from the proxy on some machines. It works for you and not for them corporate network proxy bypassed use the QGIS stack TLS error internal CA add it to the auth database internal host exclusion list respected automatically

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

python
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 QgsNetworkAccessManager or QgsBlockingNetworkRequest.
  • 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.