Loading a WFS Layer in a Background Task
Add a WFS layer to a QGIS project without freezing the window: construct the layer inside QgsTask.run because the capabilities request is synchronous,…
TL;DR: construct the QgsVectorLayer inside QgsTask.run() — the constructor performs a synchronous capabilities request that would otherwise freeze the window — and add it to the project from finished(), which runs on the main thread. This page is part of the network requests and remote data in plugins guide.
Complete Runnable Code
"""Load a WFS layer without blocking the QGIS interface."""
from urllib.parse import urlencode
from qgis.core import QgsProject, QgsTask, QgsVectorLayer
def wfs_uri(url: str, typename: str, version: str = "2.0.0",
srsname: str = "EPSG:4326", auth_cfg: str = "") -> str:
"""Build a WFS provider URI. Every value is escaped by urlencode."""
params = {"url": url, "typename": typename, "version": version,
"srsname": srsname, "restrictToRequestBBOX": "1"}
if auth_cfg:
params["authcfg"] = auth_cfg
return urlencode(params)
class LoadWfsTask(QgsTask):
"""Builds a remote layer off the main thread and registers it when finished."""
def __init__(self, uri: str, name: str):
super().__init__("Loading %s" % name, QgsTask.CanCancel)
self._uri = uri
self._name = name
self._layer: QgsVectorLayer | None = None
self._error = ""
def run(self) -> bool:
"""Worker thread: construct the layer, which performs the capabilities request."""
try:
layer = QgsVectorLayer(self._uri, self._name, "WFS")
except Exception as exc: # provider construction can raise
self._error = str(exc)
return False
if self.isCanceled():
return False
if not layer.isValid():
self._error = layer.error().summary() or "the service returned no usable layer"
return False
self._layer = layer
return True
def finished(self, ok: bool) -> None:
"""Main thread: the only place the project may be touched."""
if ok and self._layer is not None:
QgsProject.instance().addMapLayer(self._layer)
else:
print("WFS load failed: %s" % (self._error or "cancelled"))
def load_wfs(url: str, typename: str, name: str) -> LoadWfsTask:
"""Submit the load and return the task so a caller can connect to its signals."""
from qgis.core import QgsApplication
task = LoadWfsTask(wfs_uri(url, typename), name)
QgsApplication.taskManager().addTask(task)
return task
Architecture Breakdown
Why constructing the layer is the slow part
QgsVectorLayer(uri, name, "WFS") does not merely record a URL. The provider issues a GetCapabilities request, parses the response, resolves the feature type and its schema, and works out the layer’s extent and geometry type. On a healthy service that is a second; on a busy or distant one it is ten, and every one of those seconds is a frozen window if the call is on the GUI thread.
Moving it into run() costs nothing in complexity — it is the same single line — and removes the entire class of “QGIS hangs when I click the button” reports.
finished() is where the project is touched
QgsProject is a main-thread object. Adding the layer from run() may appear to work and will eventually corrupt state, because the project and the layer tree are being read by the rendering machinery at the same moment.
The task system exists to make this easy: finished() is delivered on the main thread with the result of run(), and it is the only place in this pattern where the project appears.
The URI is a query string
The WFS provider takes its configuration as a URL-encoded parameter string, which is why the helper uses urlencode rather than string concatenation. A typename containing a colon — the common namespace:layer form — must be escaped, and hand-built URIs get this wrong often enough that it is worth never doing.
restrictToRequestBBOX=1 is worth knowing about: it tells the provider to request only the current view’s extent rather than the whole dataset, which turns an unusable layer over a large service into a responsive one.
Diagnosing an Invalid Layer
An invalid remote layer gives you very little to work with by default, but the causes cluster into three groups that can be told apart with one browser request and one careful comparison.
isValid() returning False is the provider’s way of saying it could not build a usable layer, and layer.error().summary() usually names the reason. Three causes cover most cases, and they are distinguishable without guessing: fetch the capabilities document in a browser to prove the service is reachable, compare the typename exactly — they are case-sensitive and often namespaced — and try the other protocol version, since a service advertising 2.0.0 sometimes only really implements 1.1.0.
Reporting Progress to the User
from qgis.core import QgsApplication
def load_with_feedback(iface, url: str, typename: str, name: str) -> None:
"""Submit a load and report the outcome on the message bar."""
task = LoadWfsTask(wfs_uri(url, typename), name)
def done():
if task.status() == QgsTask.Complete:
iface.messageBar().pushSuccess("My Plugin", "Loaded %s" % name)
else:
iface.messageBar().pushWarning("My Plugin", "Could not load %s" % name)
task.taskCompleted.connect(done)
task.taskTerminated.connect(done)
QgsApplication.taskManager().addTask(task)
Because the task carries a description, QGIS also shows it in the task manager widget with a cancel button — which is the whole reason to prefer QgsTask over a bare thread for work a user initiated.
Keeping the Layer Usable Once It Is Loaded
Loading the layer is only half the job; a remote layer that is technically present but painfully slow to draw will be blamed on the plugin that added it. Two settings do most of the work here.
restrictToRequestBBOX limits each request to the current view, which turns a service holding millions of features into something that behaves like a local layer at every zoom level. Without it the provider may attempt to fetch the whole feature type on the first render, and on a large service that request never usefully completes.
Setting a sensible scale-visibility range is the second. A detailed feature type that is meaningless at 1:2 000 000 should simply not be requested at that scale, and telling the layer so costs one call. Between them, the two turn “the plugin made QGIS slow” into a layer that behaves the way users expect remote data to behave.
Production Best Practices
- Never construct a remote layer on the GUI thread. The capabilities request is synchronous.
- Check
isValid()insiderun()so the failure is reported, not registered. - Build the URI with
urlencode, never by concatenation. - Set
restrictToRequestBBOXfor large services, or the first render fetches everything. - Keep a reference to the task if you connect to its signals; the manager owns it, but your lambdas need it alive.
- Pass the authentication configuration id as
authcfg, so the provider applies stored credentials itself.
Frequently Asked Questions
Can I do this for other remote providers?
Yes — the same pattern applies to WMS, WCS, ArcGIS REST and OGC API layers, and to any QgsVectorLayer over a slow path such as a remote GeoPackage. What changes is only the provider key and the URI format; the thread discipline is identical.
Does the layer keep working after the task ends?
Yes. The task constructs the layer and hands it over; once registered, it belongs to the project and fetches data as the canvas needs it. Those later fetches happen on QGIS’s own threads, so they do not freeze the interface either.
What happens if the user cancels?
isCanceled() returns True at the next check and run() returns False, so finished() sees failure and does nothing. The partially constructed layer is dropped with the task. Note that cancellation cannot interrupt the capabilities request itself, so a cancel during a slow response takes effect only once it returns.
Should I cache the capabilities document?
The provider does some caching of its own, and for a service you load repeatedly it is worth enabling Qt’s disk cache rather than building your own layer. Where a plugin offers a list of available typenames, though, caching that list in settings for a session is a reasonable saving — it is a request the user pays for before they have even chosen anything.
How do I list the available layers on a service?
Fetch the capabilities document yourself and parse the feature type names out of it, using the non-blocking JSON pattern adapted for XML. Populate the chooser from that rather than making the user type a typename, which is both unfriendly and the single most common source of empty layers.
Is a memory layer a reasonable alternative?
Sometimes. Fetching features once into a memory layer gives you a snapshot with no further network traffic, which suits analysis and makes runs reproducible. The trade is that it does not update and does not scale past what fits in memory, so it is a good fit for a bounded extract and a poor one for a browsing tool.
Related
- Network Requests and Remote Data in Plugins — the parent guide covering the QGIS network stack
- Asynchronous Task Execution with QgsTask — the task machinery this page relies on
- How to Safely Load Shapefiles into QgsProject Without UI Blocking — the same handoff for local data