Make HTTP Requests with QgsNetworkAccessManager in PyQGIS
Sooner or later a PyQGIS script needs to talk to something that is not a map service: a geocoding API, a JSON endpoint listing today's sensor readings, a download link for a zipped dataset, an internal REST service that registers a finished job. Reaching for requests or urllib works on a developer's laptop and then fails on a corporate desktop, because those libraries know nothing about the proxy, the custom certificate authority or the stored credentials that QGIS has been configured with. QGIS's own network stack already knows all of that, and it is fully available from Python.
This recipe belongs to Web Services and Remote Data in PyQGIS. It makes blocking requests for scripts, asynchronous requests for plugins, downloads files with progress, and covers authentication, timeouts and POSTing JSON.
Prerequisites
- QGIS 3.40 LTR or newer, or the QGIS 4 series.
- A URL to call. The examples use a JSON API and a zipped download.
- For secured endpoints, an authentication configuration created in QGIS; see storing credentials with QgsAuthManager.
A blocking GET for scripts
QgsBlockingNetworkRequest sends a request and waits for the answer while still letting QGIS process events, which makes it the right tool in console scripts, Processing algorithms and headless jobs.
import json
from qgis.PyQt.QtCore import QUrl, QUrlQuery
from qgis.PyQt.QtNetwork import QNetworkRequest
from qgis.core import QgsBlockingNetworkRequest
def get_json(url, params=None, authcfg=None, feedback=None):
q = QUrl(url)
if params:
query = QUrlQuery()
for k, v in params.items():
query.addQueryItem(k, str(v))
q.setQuery(query)
request = QNetworkRequest(q)
request.setRawHeader(b"Accept", b"application/json")
blocking = QgsBlockingNetworkRequest()
if authcfg:
blocking.setAuthCfg(authcfg)
code = blocking.get(request, False, feedback)
reply = blocking.reply()
status = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if code != QgsBlockingNetworkRequest.NoError:
raise RuntimeError(f"HTTP {status}: {blocking.errorMessage()}")
return json.loads(bytes(reply.content()).decode("utf-8"))
stations = get_json("https://api.example.org/air-quality/stations",
{"country": "GB", "active": "true"})
print(len(stations["items"]), "stations")
Breakdown: QUrlQuery percent-encodes parameter values, so a place name with spaces or an ampersand cannot break the URL. The second argument to get forces a cache refresh when true; QGIS caches responses according to their HTTP headers, which is usually what you want for reference data and not what you want for live readings. Passing a QgsFeedback — the one a Processing algorithm receives — lets the user's Cancel button abort the request. reply() returns a QgsNetworkReplyContent, a copy of the response that stays valid after the request object goes away; content() is a QByteArray, so convert to bytes before decoding. Raising with the HTTP status included makes failures in unattended scripts diagnosable from the log alone.
POST JSON to an API
Sending data is the same object with a body and a content type.
def post_json(url, payload, authcfg=None):
request = QNetworkRequest(QUrl(url))
request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
blocking = QgsBlockingNetworkRequest()
if authcfg:
blocking.setAuthCfg(authcfg)
body = json.dumps(payload).encode("utf-8")
if blocking.post(request, body) != QgsBlockingNetworkRequest.NoError:
raise RuntimeError(blocking.errorMessage())
content = bytes(blocking.reply().content())
return json.loads(content) if content else None
result = post_json("https://jobs.example.internal/api/runs", {
"job": "nightly-flood-extent",
"status": "finished",
"features": 1842,
}, authcfg="jobs001")
Breakdown: Setting the content type header is what tells most APIs to parse the body as JSON; without it many frameworks return a 415 or silently ignore the body. post takes the body as bytes. An empty response body is common for status endpoints, hence the guard before decoding. setAuthCfg attaches whatever the configuration holds — basic credentials, an API key header, an OAuth2 token — so the script carries no secrets.
Asynchronous requests for plugins
Inside QGIS Desktop, a blocking call made from a button handler freezes the whole application until the server answers. Plugins should send the request and handle the reply when it arrives.
from qgis.PyQt.QtNetwork import QNetworkReply
from qgis.core import QgsNetworkAccessManager, QgsApplication
class StationFetcher:
def __init__(self, on_result, on_error):
self.on_result = on_result
self.on_error = on_error
self.reply = None
def start(self, url, authcfg=None):
request = QNetworkRequest(QUrl(url))
if authcfg:
QgsApplication.authManager().updateNetworkRequest(request, authcfg)
self.reply = QgsNetworkAccessManager.instance().get(request)
self.reply.finished.connect(self._finished)
def cancel(self):
if self.reply is not None:
self.reply.abort()
def _finished(self):
reply, self.reply = self.reply, None
try:
if reply.error() != QNetworkReply.NoError:
if reply.error() != QNetworkReply.OperationCanceledError:
self.on_error(reply.errorString())
return
self.on_result(json.loads(bytes(reply.readAll()).decode("utf-8")))
finally:
reply.deleteLater()
fetcher = StationFetcher(
on_result=lambda data: iface.messageBar().pushSuccess("Stations", f"{len(data['items'])} loaded"),
on_error=lambda msg: iface.messageBar().pushWarning("Stations", msg),
)
fetcher.start("https://api.example.org/air-quality/stations?country=GB")
Breakdown: QgsNetworkAccessManager.instance() is the shared manager, so this request gets the same proxy and SSL treatment as everything else. get returns immediately with a QNetworkReply; the finished signal fires later on the main thread, where it is safe to touch the interface. Keeping a reference to the fetcher — as an attribute of the plugin, not a local variable — matters, because a garbage-collected object takes its slot connection with it and the reply is never handled; the general rule is explained in QGIS object ownership and crashes. updateNetworkRequest applies an authentication configuration to a raw request. deleteLater frees the reply once handled, and treating a cancelled request as silent avoids an error message when the user closed the dialog deliberately.
Download a file with progress
QgsFileDownloader streams a response to disk, reports progress and handles redirects and authentication. It suits zipped datasets and imagery that should not be held in memory.
from qgis.PyQt.QtCore import QEventLoop
from qgis.core import QgsFileDownloader
def download(url, target, authcfg=""):
loop = QEventLoop()
errors = []
downloader = QgsFileDownloader(QUrl(url), target, authcfg, True)
downloader.downloadProgress.connect(
lambda received, total: print(f"\r{received / max(total, 1):5.0%}", end=""))
downloader.downloadError.connect(lambda messages: errors.extend(messages))
downloader.downloadExited.connect(loop.quit)
downloader.startDownload()
loop.exec()
if errors:
raise RuntimeError("; ".join(errors))
print("\nsaved", target)
download("https://data.example.org/boundaries/wards_2026.gpkg.zip",
"/data/inbox/wards_2026.gpkg.zip")
Breakdown: Passing True as the last constructor argument delays the start until startDownload, so every signal is connected before any bytes arrive. downloadExited fires after success, error or cancellation alike, which makes it the right signal to end the local event loop; the error list distinguishes the outcomes. The event loop keeps a script synchronous without blocking QGIS's own event processing. In a plugin, skip the loop and react to downloadCompleted instead. A downloaded zip can be read in place through GDAL's /vsizip/ path without extracting it.
Timeouts and retries
QGIS applies one network timeout to every request, set in Options → Network. Scripts that call slow APIs can raise it for the session, and should retry only failures that are likely to succeed on a second attempt.
import time
from qgis.core import QgsNetworkAccessManager
QgsNetworkAccessManager.setTimeout(120_000)
TRANSIENT = {502, 503, 504}
def get_json_with_retry(url, params=None, attempts=4):
for attempt in range(attempts):
try:
return get_json(url, params)
except RuntimeError as exc:
status = str(exc).split(":", 1)[0].replace("HTTP ", "")
retryable = status in {str(s) for s in TRANSIENT} or "timed out" in str(exc).lower()
if not retryable or attempt == attempts - 1:
raise
time.sleep(2 ** (attempt + 1))
Breakdown: setTimeout takes milliseconds and changes the value for the whole QGIS session, so a plugin that raises it should restore the previous value afterwards with QgsNetworkAccessManager.timeout(). Exponential backoff — two, four, eight seconds — gives an overloaded server room to recover instead of hammering it. Authentication and not-found errors are raised immediately, because retrying them only delays the inevitable and can lock an account. SSL and proxy errors need a settings change; surface them clearly rather than retrying.
QGIS version compatibility
QgsBlockingNetworkRequest arrived in QGIS 3.6 and QgsFileDownloader in 3.2; both are unchanged in 3.40 and 3.44. On the QGIS 4 series, PyQt6 requires scoped enums: QNetworkRequest.Attribute.HttpStatusCodeAttribute, QNetworkRequest.KnownHeaders.ContentTypeHeader, QNetworkReply.NetworkError.NoError, and QgsBlockingNetworkRequest.ErrorCode.NoError. QEventLoop.exec() replaces exec_(). Writing the scoped forms now keeps the code working on both.
Troubleshooting
- Works with
requestson your machine, fails in QGIS for users. Their proxy or CA is configured in QGIS; use the QGIS stack everywhere. - The reply handler never runs. The object holding the reply was garbage-collected; keep a reference.
- QGIS freezes during a request. A blocking request is running in UI code; switch to the asynchronous pattern.
- Stale data comes back. The response was cached; force a refresh or send
Cache-Control: no-cache. - SSL errors on an internal server. Add the organisation's CA certificate in QGIS's authentication settings.
Conclusion
Route every HTTP call through QGIS's network stack so proxies, certificates and stored credentials apply. Use QgsBlockingNetworkRequest in scripts and algorithms, the shared manager with a finished handler in plugin UI code, and QgsFileDownloader for files. Attach credentials with authentication configs, and retry only the failures that are genuinely transient.
Frequently Asked Questions
Can I still use requests in a plugin?
You can, but it ignores QGIS's proxy, SSL and authentication settings, and it blocks the interface unless run in a thread. The QGIS stack avoids both problems.
How do I send custom headers such as an API key?request.setRawHeader(b"X-API-Key", key.encode()) — or better, store the key in an authentication configuration using the API header method.
Can a QgsTask use these requests?
Yes. Use QgsBlockingNetworkRequest inside run(); it is safe in the task thread, as in running a long task with QgsTask.
How do I see what QGIS actually sent? Open the Network Logger in the developer tools panel; every request through the manager appears there with headers and timings.