[{"data":1,"prerenderedAt":2292},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fmake-http-requests-with-qgsnetworkaccessmanager-pyqgis":3},{"id":4,"title":5,"body":6,"description":2281,"extension":2282,"meta":2283,"navigation":302,"path":2288,"seo":2289,"stem":2290,"__hash__":2291},"docs\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fmake-http-requests-with-qgsnetworkaccessmanager-pyqgis\u002Findex.md","Make HTTP Requests with QgsNetworkAccessManager in PyQGIS",{"type":7,"value":8,"toc":2268},"minimark",[9,13,26,35,199,204,226,230,236,670,717,721,724,919,932,1031,1035,1038,1429,1460,1464,1470,1699,1723,1793,1797,1804,2078,2090,2094,2123,2127,2165,2169,2181,2185,2194,2203,2220,2230,2234,2264],[10,11,5],"h1",{"id":12},"make-http-requests-with-qgsnetworkaccessmanager-in-pyqgis",[14,15,16,17,21,22,25],"p",{},"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 ",[18,19,20],"code",{},"requests"," or ",[18,23,24],{},"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.",[14,27,28,29,34],{},"This recipe belongs to ",[30,31,33],"a",{"href":32},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002F","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.",[14,36,37],{},[38,39,44,48,52,59,76,85,94,100,103,107,110,114,122,125,128,138,144,149,153,157,161,166,174,180,183,189,195],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 300","img","Why requests go through QgsNetworkAccessManager: one shared network manager applies the proxy settings, SSL certificate configuration, authentication configs, user agent and timeout that the user configured in QGIS, for provider requests and Python requests alike","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"One network stack, configured once",[49,50,51],"desc",{},"Requests from data providers such as WMS and WFS, from QgsBlockingNetworkRequest in scripts, and from QgsFileDownloader in plugins all pass through QgsNetworkAccessManager. It applies the proxy and exclusion list, extra certificate authorities and client certificates, authentication configurations, the QGIS user agent, and the network timeout. A request made with the requests library bypasses all of these and can fail where QGIS itself succeeds.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","300","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"namStackArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Use the stack the user already configured",[53,86],{"x":87,"y":88,"width":89,"height":90,"rx":67,"fill":91,"stroke":92,"style":93},"24","52","180","40","#fffdf7","#59645f","stroke-width:1.8",[77,95,99],{"x":96,"y":97,"style":98,"fill":75,"textAnchor":83},"114","77","text-anchor:middle;font-size:10.5px;font-family:sans-serif","WMS \u002F WFS providers",[53,101],{"x":87,"y":102,"width":89,"height":90,"rx":67,"fill":91,"stroke":92,"style":93},"106",[77,104,106],{"x":96,"y":105,"style":98,"fill":75,"textAnchor":83},"131","blocking script request",[53,108],{"x":87,"y":109,"width":89,"height":90,"rx":67,"fill":91,"stroke":92,"style":93},"160",[77,111,113],{"x":96,"y":112,"style":98,"fill":75,"textAnchor":83},"185","plugin file download",[115,116],"line",{"x1":117,"y1":118,"x2":119,"y2":120,"stroke":75,"style":121},"204","72","262","118","stroke-width:1.6;marker-end:url(#namStackArrow)",[115,123],{"x1":117,"y1":124,"x2":119,"y2":124,"stroke":75,"style":121},"126",[115,126],{"x1":117,"y1":89,"x2":119,"y2":127,"stroke":75,"style":121},"134",[53,129],{"x":130,"y":131,"width":132,"height":133,"rx":134,"fill":135,"stroke":136,"style":137},"270","56","236","144","10","#eef7f4","#0f766e","stroke-width:2.5",[77,139,143],{"x":140,"y":141,"style":142,"fill":136,"textAnchor":83},"388","82","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","QgsNetworkAccessManager",[77,145,148],{"x":140,"y":146,"style":147,"fill":75,"textAnchor":83},"108","text-anchor:middle;font-size:10px;font-family:sans-serif","proxy + exclusions",[77,150,152],{"x":140,"y":151,"style":147,"fill":75,"textAnchor":83},"128","extra CAs, client certs",[77,154,156],{"x":140,"y":155,"style":147,"fill":75,"textAnchor":83},"148","auth configs",[77,158,160],{"x":140,"y":159,"style":147,"fill":75,"textAnchor":83},"168","timeout, user agent",[115,162],{"x1":163,"y1":151,"x2":164,"y2":151,"stroke":75,"style":165},"506","560","stroke-width:2;marker-end:url(#namStackArrow)",[53,167],{"x":168,"y":169,"width":159,"height":170,"rx":134,"fill":171,"stroke":172,"style":173},"568","96","64","#e8efe6","#15803d","stroke-width:2",[77,175,179],{"x":176,"y":177,"style":142,"fill":178,"textAnchor":83},"652","124","#166534","the internet",[77,181,182],{"x":176,"y":133,"style":147,"fill":75,"textAnchor":83},"via corporate proxy",[53,184],{"x":87,"y":185,"width":186,"height":88,"rx":134,"fill":187,"stroke":188,"style":173},"224","482","#fdf2e2","#b91c1c",[77,190,194],{"x":191,"y":192,"style":193,"fill":188,"textAnchor":83},"265","248","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","requests \u002F urllib bypass all of it",[77,196,198],{"x":191,"y":197,"style":147,"fill":75,"textAnchor":83},"266","works on your laptop, fails behind the user's proxy",[200,201,203],"h2",{"id":202},"prerequisites","Prerequisites",[205,206,207,215,218],"ul",{},[208,209,210,214],"li",{},[211,212,213],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series.",[208,216,217],{},"A URL to call. The examples use a JSON API and a zipped download.",[208,219,220,221,225],{},"For secured endpoints, an authentication configuration created in QGIS; see ",[30,222,224],{"href":223},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fstore-credentials-with-qgsauthmanager-pyqgis\u002F","storing credentials with QgsAuthManager",".",[200,227,229],{"id":228},"a-blocking-get-for-scripts","A blocking GET for scripts",[14,231,232,235],{},[18,233,234],{},"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.",[237,238,243],"pre",{"className":239,"code":240,"language":241,"meta":242,"style":242},"language-python shiki shiki-themes github-dark","import json\nfrom qgis.PyQt.QtCore import QUrl, QUrlQuery\nfrom qgis.PyQt.QtNetwork import QNetworkRequest\nfrom qgis.core import QgsBlockingNetworkRequest\n\ndef get_json(url, params=None, authcfg=None, feedback=None):\n    q = QUrl(url)\n    if params:\n        query = QUrlQuery()\n        for k, v in params.items():\n            query.addQueryItem(k, str(v))\n        q.setQuery(query)\n\n    request = QNetworkRequest(q)\n    request.setRawHeader(b\"Accept\", b\"application\u002Fjson\")\n\n    blocking = QgsBlockingNetworkRequest()\n    if authcfg:\n        blocking.setAuthCfg(authcfg)\n    code = blocking.get(request, False, feedback)\n    reply = blocking.reply()\n    status = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)\n\n    if code != QgsBlockingNetworkRequest.NoError:\n        raise RuntimeError(f\"HTTP {status}: {blocking.errorMessage()}\")\n    return json.loads(bytes(reply.content()).decode(\"utf-8\"))\n\nstations = get_json(\"https:\u002F\u002Fapi.example.org\u002Fair-quality\u002Fstations\",\n                    {\"country\": \"GB\", \"active\": \"true\"})\nprint(len(stations[\"items\"]), \"stations\")\n","python","",[18,244,245,257,271,284,297,304,341,352,361,372,387,399,405,410,421,445,450,461,469,475,492,503,514,519,533,575,596,601,618,645],{"__ignoreMap":242},[246,247,249,253],"span",{"class":115,"line":248},1,[246,250,252],{"class":251},"snl16","import",[246,254,256],{"class":255},"s95oV"," json\n",[246,258,260,263,266,268],{"class":115,"line":259},2,[246,261,262],{"class":251},"from",[246,264,265],{"class":255}," qgis.PyQt.QtCore ",[246,267,252],{"class":251},[246,269,270],{"class":255}," QUrl, QUrlQuery\n",[246,272,274,276,279,281],{"class":115,"line":273},3,[246,275,262],{"class":251},[246,277,278],{"class":255}," qgis.PyQt.QtNetwork ",[246,280,252],{"class":251},[246,282,283],{"class":255}," QNetworkRequest\n",[246,285,287,289,292,294],{"class":115,"line":286},4,[246,288,262],{"class":251},[246,290,291],{"class":255}," qgis.core ",[246,293,252],{"class":251},[246,295,296],{"class":255}," QgsBlockingNetworkRequest\n",[246,298,300],{"class":115,"line":299},5,[246,301,303],{"emptyLinePlaceholder":302},true,"\n",[246,305,307,310,314,317,320,324,327,329,331,334,336,338],{"class":115,"line":306},6,[246,308,309],{"class":251},"def",[246,311,313],{"class":312},"svObZ"," get_json",[246,315,316],{"class":255},"(url, params",[246,318,319],{"class":251},"=",[246,321,323],{"class":322},"sDLfK","None",[246,325,326],{"class":255},", authcfg",[246,328,319],{"class":251},[246,330,323],{"class":322},[246,332,333],{"class":255},", feedback",[246,335,319],{"class":251},[246,337,323],{"class":322},[246,339,340],{"class":255},"):\n",[246,342,344,347,349],{"class":115,"line":343},7,[246,345,346],{"class":255},"    q ",[246,348,319],{"class":251},[246,350,351],{"class":255}," QUrl(url)\n",[246,353,355,358],{"class":115,"line":354},8,[246,356,357],{"class":251},"    if",[246,359,360],{"class":255}," params:\n",[246,362,364,367,369],{"class":115,"line":363},9,[246,365,366],{"class":255},"        query ",[246,368,319],{"class":251},[246,370,371],{"class":255}," QUrlQuery()\n",[246,373,375,378,381,384],{"class":115,"line":374},10,[246,376,377],{"class":251},"        for",[246,379,380],{"class":255}," k, v ",[246,382,383],{"class":251},"in",[246,385,386],{"class":255}," params.items():\n",[246,388,390,393,396],{"class":115,"line":389},11,[246,391,392],{"class":255},"            query.addQueryItem(k, ",[246,394,395],{"class":322},"str",[246,397,398],{"class":255},"(v))\n",[246,400,402],{"class":115,"line":401},12,[246,403,404],{"class":255},"        q.setQuery(query)\n",[246,406,408],{"class":115,"line":407},13,[246,409,303],{"emptyLinePlaceholder":302},[246,411,413,416,418],{"class":115,"line":412},14,[246,414,415],{"class":255},"    request ",[246,417,319],{"class":251},[246,419,420],{"class":255}," QNetworkRequest(q)\n",[246,422,424,427,430,434,437,439,442],{"class":115,"line":423},15,[246,425,426],{"class":255},"    request.setRawHeader(",[246,428,429],{"class":251},"b",[246,431,433],{"class":432},"sU2Wk","\"Accept\"",[246,435,436],{"class":255},", ",[246,438,429],{"class":251},[246,440,441],{"class":432},"\"application\u002Fjson\"",[246,443,444],{"class":255},")\n",[246,446,448],{"class":115,"line":447},16,[246,449,303],{"emptyLinePlaceholder":302},[246,451,453,456,458],{"class":115,"line":452},17,[246,454,455],{"class":255},"    blocking ",[246,457,319],{"class":251},[246,459,460],{"class":255}," QgsBlockingNetworkRequest()\n",[246,462,464,466],{"class":115,"line":463},18,[246,465,357],{"class":251},[246,467,468],{"class":255}," authcfg:\n",[246,470,472],{"class":115,"line":471},19,[246,473,474],{"class":255},"        blocking.setAuthCfg(authcfg)\n",[246,476,478,481,483,486,489],{"class":115,"line":477},20,[246,479,480],{"class":255},"    code ",[246,482,319],{"class":251},[246,484,485],{"class":255}," blocking.get(request, ",[246,487,488],{"class":322},"False",[246,490,491],{"class":255},", feedback)\n",[246,493,495,498,500],{"class":115,"line":494},21,[246,496,497],{"class":255},"    reply ",[246,499,319],{"class":251},[246,501,502],{"class":255}," blocking.reply()\n",[246,504,506,509,511],{"class":115,"line":505},22,[246,507,508],{"class":255},"    status ",[246,510,319],{"class":251},[246,512,513],{"class":255}," reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)\n",[246,515,517],{"class":115,"line":516},23,[246,518,303],{"emptyLinePlaceholder":302},[246,520,522,524,527,530],{"class":115,"line":521},24,[246,523,357],{"class":251},[246,525,526],{"class":255}," code ",[246,528,529],{"class":251},"!=",[246,531,532],{"class":255}," QgsBlockingNetworkRequest.NoError:\n",[246,534,536,539,542,545,548,551,554,557,560,563,565,568,570,573],{"class":115,"line":535},25,[246,537,538],{"class":251},"        raise",[246,540,541],{"class":322}," RuntimeError",[246,543,544],{"class":255},"(",[246,546,547],{"class":251},"f",[246,549,550],{"class":432},"\"HTTP ",[246,552,553],{"class":322},"{",[246,555,556],{"class":255},"status",[246,558,559],{"class":322},"}",[246,561,562],{"class":432},": ",[246,564,553],{"class":322},[246,566,567],{"class":255},"blocking.errorMessage()",[246,569,559],{"class":322},[246,571,572],{"class":432},"\"",[246,574,444],{"class":255},[246,576,578,581,584,587,590,593],{"class":115,"line":577},26,[246,579,580],{"class":251},"    return",[246,582,583],{"class":255}," json.loads(",[246,585,586],{"class":322},"bytes",[246,588,589],{"class":255},"(reply.content()).decode(",[246,591,592],{"class":432},"\"utf-8\"",[246,594,595],{"class":255},"))\n",[246,597,599],{"class":115,"line":598},27,[246,600,303],{"emptyLinePlaceholder":302},[246,602,604,607,609,612,615],{"class":115,"line":603},28,[246,605,606],{"class":255},"stations ",[246,608,319],{"class":251},[246,610,611],{"class":255}," get_json(",[246,613,614],{"class":432},"\"https:\u002F\u002Fapi.example.org\u002Fair-quality\u002Fstations\"",[246,616,617],{"class":255},",\n",[246,619,621,624,627,629,632,634,637,639,642],{"class":115,"line":620},29,[246,622,623],{"class":255},"                    {",[246,625,626],{"class":432},"\"country\"",[246,628,562],{"class":255},[246,630,631],{"class":432},"\"GB\"",[246,633,436],{"class":255},[246,635,636],{"class":432},"\"active\"",[246,638,562],{"class":255},[246,640,641],{"class":432},"\"true\"",[246,643,644],{"class":255},"})\n",[246,646,648,651,653,656,659,662,665,668],{"class":115,"line":647},30,[246,649,650],{"class":322},"print",[246,652,544],{"class":255},[246,654,655],{"class":322},"len",[246,657,658],{"class":255},"(stations[",[246,660,661],{"class":432},"\"items\"",[246,663,664],{"class":255},"]), ",[246,666,667],{"class":432},"\"stations\"",[246,669,444],{"class":255},[14,671,672,675,676,679,680,683,684,687,688,692,693,696,697,700,701,704,705,708,709,711,712,716],{},[211,673,674],{},"Breakdown:"," ",[18,677,678],{},"QUrlQuery"," percent-encodes parameter values, so a place name with spaces or an ampersand cannot break the URL. The second argument to ",[18,681,682],{},"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 ",[18,685,686],{},"QgsFeedback"," — the one a Processing algorithm receives — lets the user's ",[689,690,691],"em",{},"Cancel"," button abort the request. ",[18,694,695],{},"reply()"," returns a ",[18,698,699],{},"QgsNetworkReplyContent",", a copy of the response that stays valid after the request object goes away; ",[18,702,703],{},"content()"," is a ",[18,706,707],{},"QByteArray",", so convert to ",[18,710,586],{}," before decoding. Raising with the HTTP status included makes failures in ",[30,713,715],{"href":714},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fhandle-errors-and-logging-in-unattended-scripts\u002F","unattended scripts"," diagnosable from the log alone.",[200,718,720],{"id":719},"post-json-to-an-api","POST JSON to an API",[14,722,723],{},"Sending data is the same object with a body and a content type.",[237,725,727],{"className":239,"code":726,"language":241,"meta":242,"style":242},"def post_json(url, payload, authcfg=None):\n    request = QNetworkRequest(QUrl(url))\n    request.setHeader(QNetworkRequest.ContentTypeHeader, \"application\u002Fjson\")\n    blocking = QgsBlockingNetworkRequest()\n    if authcfg:\n        blocking.setAuthCfg(authcfg)\n    body = json.dumps(payload).encode(\"utf-8\")\n    if blocking.post(request, body) != QgsBlockingNetworkRequest.NoError:\n        raise RuntimeError(blocking.errorMessage())\n    content = bytes(blocking.reply().content())\n    return json.loads(content) if content else None\n\nresult = post_json(\"https:\u002F\u002Fjobs.example.internal\u002Fapi\u002Fruns\", {\n    \"job\": \"nightly-flood-extent\",\n    \"status\": \"finished\",\n    \"features\": 1842,\n}, authcfg=\"jobs001\")\n",[18,728,729,745,754,763,771,777,781,795,806,815,828,847,851,867,879,891,903],{"__ignoreMap":242},[246,730,731,733,736,739,741,743],{"class":115,"line":248},[246,732,309],{"class":251},[246,734,735],{"class":312}," post_json",[246,737,738],{"class":255},"(url, payload, authcfg",[246,740,319],{"class":251},[246,742,323],{"class":322},[246,744,340],{"class":255},[246,746,747,749,751],{"class":115,"line":259},[246,748,415],{"class":255},[246,750,319],{"class":251},[246,752,753],{"class":255}," QNetworkRequest(QUrl(url))\n",[246,755,756,759,761],{"class":115,"line":273},[246,757,758],{"class":255},"    request.setHeader(QNetworkRequest.ContentTypeHeader, ",[246,760,441],{"class":432},[246,762,444],{"class":255},[246,764,765,767,769],{"class":115,"line":286},[246,766,455],{"class":255},[246,768,319],{"class":251},[246,770,460],{"class":255},[246,772,773,775],{"class":115,"line":299},[246,774,357],{"class":251},[246,776,468],{"class":255},[246,778,779],{"class":115,"line":306},[246,780,474],{"class":255},[246,782,783,786,788,791,793],{"class":115,"line":343},[246,784,785],{"class":255},"    body ",[246,787,319],{"class":251},[246,789,790],{"class":255}," json.dumps(payload).encode(",[246,792,592],{"class":432},[246,794,444],{"class":255},[246,796,797,799,802,804],{"class":115,"line":354},[246,798,357],{"class":251},[246,800,801],{"class":255}," blocking.post(request, body) ",[246,803,529],{"class":251},[246,805,532],{"class":255},[246,807,808,810,812],{"class":115,"line":363},[246,809,538],{"class":251},[246,811,541],{"class":322},[246,813,814],{"class":255},"(blocking.errorMessage())\n",[246,816,817,820,822,825],{"class":115,"line":374},[246,818,819],{"class":255},"    content ",[246,821,319],{"class":251},[246,823,824],{"class":322}," bytes",[246,826,827],{"class":255},"(blocking.reply().content())\n",[246,829,830,832,835,838,841,844],{"class":115,"line":389},[246,831,580],{"class":251},[246,833,834],{"class":255}," json.loads(content) ",[246,836,837],{"class":251},"if",[246,839,840],{"class":255}," content ",[246,842,843],{"class":251},"else",[246,845,846],{"class":322}," None\n",[246,848,849],{"class":115,"line":401},[246,850,303],{"emptyLinePlaceholder":302},[246,852,853,856,858,861,864],{"class":115,"line":407},[246,854,855],{"class":255},"result ",[246,857,319],{"class":251},[246,859,860],{"class":255}," post_json(",[246,862,863],{"class":432},"\"https:\u002F\u002Fjobs.example.internal\u002Fapi\u002Fruns\"",[246,865,866],{"class":255},", {\n",[246,868,869,872,874,877],{"class":115,"line":412},[246,870,871],{"class":432},"    \"job\"",[246,873,562],{"class":255},[246,875,876],{"class":432},"\"nightly-flood-extent\"",[246,878,617],{"class":255},[246,880,881,884,886,889],{"class":115,"line":423},[246,882,883],{"class":432},"    \"status\"",[246,885,562],{"class":255},[246,887,888],{"class":432},"\"finished\"",[246,890,617],{"class":255},[246,892,893,896,898,901],{"class":115,"line":447},[246,894,895],{"class":432},"    \"features\"",[246,897,562],{"class":255},[246,899,900],{"class":322},"1842",[246,902,617],{"class":255},[246,904,905,908,912,914,917],{"class":115,"line":452},[246,906,907],{"class":255},"}, ",[246,909,911],{"class":910},"s9osk","authcfg",[246,913,319],{"class":251},[246,915,916],{"class":432},"\"jobs001\"",[246,918,444],{"class":255},[14,920,921,923,924,927,928,931],{},[211,922,674],{}," 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. ",[18,925,926],{},"post"," takes the body as bytes. An empty response body is common for status endpoints, hence the guard before decoding. ",[18,929,930],{},"setAuthCfg"," attaches whatever the configuration holds — basic credentials, an API key header, an OAuth2 token — so the script carries no secrets.",[14,933,934],{},[38,935,938,941,944,947,951,956,962,967,971,977,983,986,991,994,998,1001,1005,1009,1012,1016,1020,1023,1025,1028],{"viewBox":936,"role":41,"ariaLabel":937,"xmlns":43},"0 0 760 256","Blocking versus asynchronous requests in QGIS Desktop: a blocking request inside a plugin button handler freezes the interface until the server responds, an asynchronous request returns immediately and handles the reply in a slot",[45,939,940],{},"Do not freeze the user's QGIS",[49,942,943],{},"Timeline on the left: a button click starts a blocking request, the interface is frozen for 8 seconds while the server responds slowly, then the result appears. Timeline on the right: a button click starts an asynchronous get that returns immediately, the user keeps panning the map, and a finished signal 8 seconds later triggers the handler that updates the plugin panel.",[53,945],{"x":55,"y":55,"width":56,"height":946,"fill":58},"256",[77,948,950],{"x":79,"y":949,"style":81,"fill":82,"textAnchor":83},"26","Same 8-second server, very different experience",[53,952],{"x":87,"y":953,"width":954,"height":955,"rx":134,"fill":187,"stroke":188,"style":137},"44","344","192",[77,957,961],{"x":958,"y":959,"style":960,"fill":188,"textAnchor":83},"196","68","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","blocking, in a plugin",[115,963],{"x1":964,"y1":151,"x2":954,"y2":151,"stroke":965,"style":966},"48","#9aa39d","stroke-width:3",[968,969],"circle",{"cx":170,"cy":151,"r":69,"fill":970},"#2563eb",[53,972],{"x":118,"y":973,"width":974,"height":87,"rx":975,"fill":188,"style":976},"116","220","4","opacity:0.85",[77,978,982],{"x":979,"y":980,"style":981,"fill":91,"textAnchor":83},"182","133","text-anchor:middle;font-size:10px;font-weight:bold;font-family:sans-serif","interface frozen",[968,984],{"cx":985,"cy":151,"r":69,"fill":172},"304",[77,987,990],{"x":170,"y":988,"style":989,"fill":75,"textAnchor":83},"164","text-anchor:middle;font-size:9.5px;font-family:sans-serif","click",[77,992,993],{"x":985,"y":988,"style":989,"fill":75,"textAnchor":83},"result",[77,995,997],{"x":958,"y":996,"style":147,"fill":92,"textAnchor":83},"208","fine in scripts and algorithms",[53,999],{"x":1000,"y":953,"width":954,"height":955,"rx":134,"fill":171,"stroke":172,"style":137},"392",[77,1002,1004],{"x":1003,"y":959,"style":960,"fill":178,"textAnchor":83},"564","asynchronous",[115,1006],{"x1":1007,"y1":151,"x2":1008,"y2":151,"stroke":965,"style":966},"416","712",[968,1010],{"cx":1011,"cy":151,"r":69,"fill":970},"432",[53,1013],{"x":1014,"y":973,"width":974,"height":87,"rx":975,"fill":1015},"440","#c9d6cf",[77,1017,1019],{"x":1018,"y":980,"style":147,"fill":75,"textAnchor":83},"550","user keeps working",[968,1021],{"cx":1022,"cy":151,"r":69,"fill":172},"672",[77,1024,990],{"x":1011,"y":988,"style":989,"fill":75,"textAnchor":83},[77,1026,1027],{"x":1022,"y":988,"style":989,"fill":75,"textAnchor":83},"finished",[77,1029,1030],{"x":1003,"y":996,"style":147,"fill":92,"textAnchor":83},"the right choice in plugin UI code",[200,1032,1034],{"id":1033},"asynchronous-requests-for-plugins","Asynchronous requests for plugins",[14,1036,1037],{},"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.",[237,1039,1041],{"className":239,"code":1040,"language":241,"meta":242,"style":242},"from qgis.PyQt.QtNetwork import QNetworkReply\nfrom qgis.core import QgsNetworkAccessManager, QgsApplication\n\nclass StationFetcher:\n    def __init__(self, on_result, on_error):\n        self.on_result = on_result\n        self.on_error = on_error\n        self.reply = None\n\n    def start(self, url, authcfg=None):\n        request = QNetworkRequest(QUrl(url))\n        if authcfg:\n            QgsApplication.authManager().updateNetworkRequest(request, authcfg)\n        self.reply = QgsNetworkAccessManager.instance().get(request)\n        self.reply.finished.connect(self._finished)\n\n    def cancel(self):\n        if self.reply is not None:\n            self.reply.abort()\n\n    def _finished(self):\n        reply, self.reply = self.reply, None\n        try:\n            if reply.error() != QNetworkReply.NoError:\n                if reply.error() != QNetworkReply.OperationCanceledError:\n                    self.on_error(reply.errorString())\n                return\n            self.on_result(json.loads(bytes(reply.readAll()).decode(\"utf-8\")))\n        finally:\n            reply.deleteLater()\n\nfetcher = StationFetcher(\n    on_result=lambda data: iface.messageBar().pushSuccess(\"Stations\", f\"{len(data['items'])} loaded\"),\n    on_error=lambda msg: iface.messageBar().pushWarning(\"Stations\", msg),\n)\nfetcher.start(\"https:\u002F\u002Fapi.example.org\u002Fair-quality\u002Fstations?country=GB\")\n",[18,1042,1043,1054,1065,1069,1080,1091,1104,1116,1127,1131,1147,1156,1163,1168,1179,1192,1196,1206,1226,1234,1238,1247,1266,1273,1286,1298,1306,1311,1328,1335,1340,1345,1356,1397,1413,1418],{"__ignoreMap":242},[246,1044,1045,1047,1049,1051],{"class":115,"line":248},[246,1046,262],{"class":251},[246,1048,278],{"class":255},[246,1050,252],{"class":251},[246,1052,1053],{"class":255}," QNetworkReply\n",[246,1055,1056,1058,1060,1062],{"class":115,"line":259},[246,1057,262],{"class":251},[246,1059,291],{"class":255},[246,1061,252],{"class":251},[246,1063,1064],{"class":255}," QgsNetworkAccessManager, QgsApplication\n",[246,1066,1067],{"class":115,"line":273},[246,1068,303],{"emptyLinePlaceholder":302},[246,1070,1071,1074,1077],{"class":115,"line":286},[246,1072,1073],{"class":251},"class",[246,1075,1076],{"class":312}," StationFetcher",[246,1078,1079],{"class":255},":\n",[246,1081,1082,1085,1088],{"class":115,"line":299},[246,1083,1084],{"class":251},"    def",[246,1086,1087],{"class":322}," __init__",[246,1089,1090],{"class":255},"(self, on_result, on_error):\n",[246,1092,1093,1096,1099,1101],{"class":115,"line":306},[246,1094,1095],{"class":322},"        self",[246,1097,1098],{"class":255},".on_result ",[246,1100,319],{"class":251},[246,1102,1103],{"class":255}," on_result\n",[246,1105,1106,1108,1111,1113],{"class":115,"line":343},[246,1107,1095],{"class":322},[246,1109,1110],{"class":255},".on_error ",[246,1112,319],{"class":251},[246,1114,1115],{"class":255}," on_error\n",[246,1117,1118,1120,1123,1125],{"class":115,"line":354},[246,1119,1095],{"class":322},[246,1121,1122],{"class":255},".reply ",[246,1124,319],{"class":251},[246,1126,846],{"class":322},[246,1128,1129],{"class":115,"line":363},[246,1130,303],{"emptyLinePlaceholder":302},[246,1132,1133,1135,1138,1141,1143,1145],{"class":115,"line":374},[246,1134,1084],{"class":251},[246,1136,1137],{"class":312}," start",[246,1139,1140],{"class":255},"(self, url, authcfg",[246,1142,319],{"class":251},[246,1144,323],{"class":322},[246,1146,340],{"class":255},[246,1148,1149,1152,1154],{"class":115,"line":389},[246,1150,1151],{"class":255},"        request ",[246,1153,319],{"class":251},[246,1155,753],{"class":255},[246,1157,1158,1161],{"class":115,"line":401},[246,1159,1160],{"class":251},"        if",[246,1162,468],{"class":255},[246,1164,1165],{"class":115,"line":407},[246,1166,1167],{"class":255},"            QgsApplication.authManager().updateNetworkRequest(request, authcfg)\n",[246,1169,1170,1172,1174,1176],{"class":115,"line":412},[246,1171,1095],{"class":322},[246,1173,1122],{"class":255},[246,1175,319],{"class":251},[246,1177,1178],{"class":255}," QgsNetworkAccessManager.instance().get(request)\n",[246,1180,1181,1183,1186,1189],{"class":115,"line":423},[246,1182,1095],{"class":322},[246,1184,1185],{"class":255},".reply.finished.connect(",[246,1187,1188],{"class":322},"self",[246,1190,1191],{"class":255},"._finished)\n",[246,1193,1194],{"class":115,"line":447},[246,1195,303],{"emptyLinePlaceholder":302},[246,1197,1198,1200,1203],{"class":115,"line":452},[246,1199,1084],{"class":251},[246,1201,1202],{"class":312}," cancel",[246,1204,1205],{"class":255},"(self):\n",[246,1207,1208,1210,1213,1215,1218,1221,1224],{"class":115,"line":463},[246,1209,1160],{"class":251},[246,1211,1212],{"class":322}," self",[246,1214,1122],{"class":255},[246,1216,1217],{"class":251},"is",[246,1219,1220],{"class":251}," not",[246,1222,1223],{"class":322}," None",[246,1225,1079],{"class":255},[246,1227,1228,1231],{"class":115,"line":471},[246,1229,1230],{"class":322},"            self",[246,1232,1233],{"class":255},".reply.abort()\n",[246,1235,1236],{"class":115,"line":477},[246,1237,303],{"emptyLinePlaceholder":302},[246,1239,1240,1242,1245],{"class":115,"line":494},[246,1241,1084],{"class":251},[246,1243,1244],{"class":312}," _finished",[246,1246,1205],{"class":255},[246,1248,1249,1252,1254,1256,1258,1260,1263],{"class":115,"line":505},[246,1250,1251],{"class":255},"        reply, ",[246,1253,1188],{"class":322},[246,1255,1122],{"class":255},[246,1257,319],{"class":251},[246,1259,1212],{"class":322},[246,1261,1262],{"class":255},".reply, ",[246,1264,1265],{"class":322},"None\n",[246,1267,1268,1271],{"class":115,"line":516},[246,1269,1270],{"class":251},"        try",[246,1272,1079],{"class":255},[246,1274,1275,1278,1281,1283],{"class":115,"line":521},[246,1276,1277],{"class":251},"            if",[246,1279,1280],{"class":255}," reply.error() ",[246,1282,529],{"class":251},[246,1284,1285],{"class":255}," QNetworkReply.NoError:\n",[246,1287,1288,1291,1293,1295],{"class":115,"line":535},[246,1289,1290],{"class":251},"                if",[246,1292,1280],{"class":255},[246,1294,529],{"class":251},[246,1296,1297],{"class":255}," QNetworkReply.OperationCanceledError:\n",[246,1299,1300,1303],{"class":115,"line":577},[246,1301,1302],{"class":322},"                    self",[246,1304,1305],{"class":255},".on_error(reply.errorString())\n",[246,1307,1308],{"class":115,"line":598},[246,1309,1310],{"class":251},"                return\n",[246,1312,1313,1315,1318,1320,1323,1325],{"class":115,"line":603},[246,1314,1230],{"class":322},[246,1316,1317],{"class":255},".on_result(json.loads(",[246,1319,586],{"class":322},[246,1321,1322],{"class":255},"(reply.readAll()).decode(",[246,1324,592],{"class":432},[246,1326,1327],{"class":255},")))\n",[246,1329,1330,1333],{"class":115,"line":620},[246,1331,1332],{"class":251},"        finally",[246,1334,1079],{"class":255},[246,1336,1337],{"class":115,"line":647},[246,1338,1339],{"class":255},"            reply.deleteLater()\n",[246,1341,1343],{"class":115,"line":1342},31,[246,1344,303],{"emptyLinePlaceholder":302},[246,1346,1348,1351,1353],{"class":115,"line":1347},32,[246,1349,1350],{"class":255},"fetcher ",[246,1352,319],{"class":251},[246,1354,1355],{"class":255}," StationFetcher(\n",[246,1357,1359,1362,1365,1368,1371,1373,1375,1377,1380,1383,1386,1389,1391,1394],{"class":115,"line":1358},33,[246,1360,1361],{"class":910},"    on_result",[246,1363,1364],{"class":251},"=lambda",[246,1366,1367],{"class":255}," data: iface.messageBar().pushSuccess(",[246,1369,1370],{"class":432},"\"Stations\"",[246,1372,436],{"class":255},[246,1374,547],{"class":251},[246,1376,572],{"class":432},[246,1378,1379],{"class":322},"{len",[246,1381,1382],{"class":255},"(data[",[246,1384,1385],{"class":432},"'items'",[246,1387,1388],{"class":255},"])",[246,1390,559],{"class":322},[246,1392,1393],{"class":432}," loaded\"",[246,1395,1396],{"class":255},"),\n",[246,1398,1400,1403,1405,1408,1410],{"class":115,"line":1399},34,[246,1401,1402],{"class":910},"    on_error",[246,1404,1364],{"class":251},[246,1406,1407],{"class":255}," msg: iface.messageBar().pushWarning(",[246,1409,1370],{"class":432},[246,1411,1412],{"class":255},", msg),\n",[246,1414,1416],{"class":115,"line":1415},35,[246,1417,444],{"class":255},[246,1419,1421,1424,1427],{"class":115,"line":1420},36,[246,1422,1423],{"class":255},"fetcher.start(",[246,1425,1426],{"class":432},"\"https:\u002F\u002Fapi.example.org\u002Fair-quality\u002Fstations?country=GB\"",[246,1428,444],{"class":255},[14,1430,1431,675,1433,1436,1437,1439,1440,1443,1444,1446,1447,1451,1452,1455,1456,1459],{},[211,1432,674],{},[18,1434,1435],{},"QgsNetworkAccessManager.instance()"," is the shared manager, so this request gets the same proxy and SSL treatment as everything else. ",[18,1438,682],{}," returns immediately with a ",[18,1441,1442],{},"QNetworkReply","; the ",[18,1445,1027],{}," 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 ",[30,1448,1450],{"href":1449},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-object-ownership-and-crashes-pyqgis\u002F","QGIS object ownership and crashes",". ",[18,1453,1454],{},"updateNetworkRequest"," applies an authentication configuration to a raw request. ",[18,1457,1458],{},"deleteLater"," frees the reply once handled, and treating a cancelled request as silent avoids an error message when the user closed the dialog deliberately.",[200,1461,1463],{"id":1462},"download-a-file-with-progress","Download a file with progress",[14,1465,1466,1469],{},[18,1467,1468],{},"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.",[237,1471,1473],{"className":239,"code":1472,"language":241,"meta":242,"style":242},"from qgis.PyQt.QtCore import QEventLoop\nfrom qgis.core import QgsFileDownloader\n\ndef download(url, target, authcfg=\"\"):\n    loop = QEventLoop()\n    errors = []\n    downloader = QgsFileDownloader(QUrl(url), target, authcfg, True)\n    downloader.downloadProgress.connect(\n        lambda received, total: print(f\"\\r{received \u002F max(total, 1):5.0%}\", end=\"\"))\n    downloader.downloadError.connect(lambda messages: errors.extend(messages))\n    downloader.downloadExited.connect(loop.quit)\n    downloader.startDownload()\n    loop.exec()\n    if errors:\n        raise RuntimeError(\"; \".join(errors))\n    print(\"\\nsaved\", target)\n\ndownload(\"https:\u002F\u002Fdata.example.org\u002Fboundaries\u002Fwards_2026.gpkg.zip\",\n         \"\u002Fdata\u002Finbox\u002Fwards_2026.gpkg.zip\")\n",[18,1474,1475,1486,1497,1501,1518,1528,1538,1553,1558,1613,1624,1629,1634,1639,1646,1660,1678,1682,1692],{"__ignoreMap":242},[246,1476,1477,1479,1481,1483],{"class":115,"line":248},[246,1478,262],{"class":251},[246,1480,265],{"class":255},[246,1482,252],{"class":251},[246,1484,1485],{"class":255}," QEventLoop\n",[246,1487,1488,1490,1492,1494],{"class":115,"line":259},[246,1489,262],{"class":251},[246,1491,291],{"class":255},[246,1493,252],{"class":251},[246,1495,1496],{"class":255}," QgsFileDownloader\n",[246,1498,1499],{"class":115,"line":273},[246,1500,303],{"emptyLinePlaceholder":302},[246,1502,1503,1505,1508,1511,1513,1516],{"class":115,"line":286},[246,1504,309],{"class":251},[246,1506,1507],{"class":312}," download",[246,1509,1510],{"class":255},"(url, target, authcfg",[246,1512,319],{"class":251},[246,1514,1515],{"class":432},"\"\"",[246,1517,340],{"class":255},[246,1519,1520,1523,1525],{"class":115,"line":299},[246,1521,1522],{"class":255},"    loop ",[246,1524,319],{"class":251},[246,1526,1527],{"class":255}," QEventLoop()\n",[246,1529,1530,1533,1535],{"class":115,"line":306},[246,1531,1532],{"class":255},"    errors ",[246,1534,319],{"class":251},[246,1536,1537],{"class":255}," []\n",[246,1539,1540,1543,1545,1548,1551],{"class":115,"line":343},[246,1541,1542],{"class":255},"    downloader ",[246,1544,319],{"class":251},[246,1546,1547],{"class":255}," QgsFileDownloader(QUrl(url), target, authcfg, ",[246,1549,1550],{"class":322},"True",[246,1552,444],{"class":255},[246,1554,1555],{"class":115,"line":354},[246,1556,1557],{"class":255},"    downloader.downloadProgress.connect(\n",[246,1559,1560,1563,1566,1568,1570,1572,1574,1577,1580,1583,1586,1589,1592,1595,1598,1600,1602,1604,1607,1609,1611],{"class":115,"line":363},[246,1561,1562],{"class":251},"        lambda",[246,1564,1565],{"class":255}," received, total: ",[246,1567,650],{"class":322},[246,1569,544],{"class":255},[246,1571,547],{"class":251},[246,1573,572],{"class":432},[246,1575,1576],{"class":322},"\\r{",[246,1578,1579],{"class":255},"received ",[246,1581,1582],{"class":251},"\u002F",[246,1584,1585],{"class":322}," max",[246,1587,1588],{"class":255},"(total, ",[246,1590,1591],{"class":322},"1",[246,1593,1594],{"class":255},")",[246,1596,1597],{"class":251},":5.0%",[246,1599,559],{"class":322},[246,1601,572],{"class":432},[246,1603,436],{"class":255},[246,1605,1606],{"class":910},"end",[246,1608,319],{"class":251},[246,1610,1515],{"class":432},[246,1612,595],{"class":255},[246,1614,1615,1618,1621],{"class":115,"line":374},[246,1616,1617],{"class":255},"    downloader.downloadError.connect(",[246,1619,1620],{"class":251},"lambda",[246,1622,1623],{"class":255}," messages: errors.extend(messages))\n",[246,1625,1626],{"class":115,"line":389},[246,1627,1628],{"class":255},"    downloader.downloadExited.connect(loop.quit)\n",[246,1630,1631],{"class":115,"line":401},[246,1632,1633],{"class":255},"    downloader.startDownload()\n",[246,1635,1636],{"class":115,"line":407},[246,1637,1638],{"class":255},"    loop.exec()\n",[246,1640,1641,1643],{"class":115,"line":412},[246,1642,357],{"class":251},[246,1644,1645],{"class":255}," errors:\n",[246,1647,1648,1650,1652,1654,1657],{"class":115,"line":423},[246,1649,538],{"class":251},[246,1651,541],{"class":322},[246,1653,544],{"class":255},[246,1655,1656],{"class":432},"\"; \"",[246,1658,1659],{"class":255},".join(errors))\n",[246,1661,1662,1665,1667,1669,1672,1675],{"class":115,"line":447},[246,1663,1664],{"class":322},"    print",[246,1666,544],{"class":255},[246,1668,572],{"class":432},[246,1670,1671],{"class":322},"\\n",[246,1673,1674],{"class":432},"saved\"",[246,1676,1677],{"class":255},", target)\n",[246,1679,1680],{"class":115,"line":452},[246,1681,303],{"emptyLinePlaceholder":302},[246,1683,1684,1687,1690],{"class":115,"line":463},[246,1685,1686],{"class":255},"download(",[246,1688,1689],{"class":432},"\"https:\u002F\u002Fdata.example.org\u002Fboundaries\u002Fwards_2026.gpkg.zip\"",[246,1691,617],{"class":255},[246,1693,1694,1697],{"class":115,"line":471},[246,1695,1696],{"class":432},"         \"\u002Fdata\u002Finbox\u002Fwards_2026.gpkg.zip\"",[246,1698,444],{"class":255},[14,1700,1701,1703,1704,1706,1707,1710,1711,1714,1715,1718,1719,1722],{},[211,1702,674],{}," Passing ",[18,1705,1550],{}," as the last constructor argument delays the start until ",[18,1708,1709],{},"startDownload",", so every signal is connected before any bytes arrive. ",[18,1712,1713],{},"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 ",[18,1716,1717],{},"downloadCompleted"," instead. A downloaded zip can be read in place through GDAL's ",[18,1720,1721],{},"\u002Fvsizip\u002F"," path without extracting it.",[14,1724,1725],{},[38,1726,1729,1732,1735,1737,1740,1744,1750,1754,1757,1761,1764,1767,1770,1773,1776,1780,1784,1787,1790],{"viewBox":1727,"role":41,"ariaLabel":1728,"xmlns":43},"0 0 760 236","Timeouts and retries for unreliable endpoints: the global network timeout applies to every request, a script can retry transient failures with backoff, and permanent errors such as 401 and 404 should fail immediately",[45,1730,1731],{},"Retry what is transient, fail on what is not",[49,1733,1734],{},"Three categories of failure. Transient: timeouts, 502, 503 and 504, and connection resets, which are worth retrying after waiting 2, 4 and 8 seconds. Permanent: 400, 401, 403 and 404, which will fail the same way every time and should stop the script with a clear message. Configuration: SSL handshake and proxy authentication failures, which need a settings change rather than a retry.",[53,1736],{"x":55,"y":55,"width":56,"height":132,"fill":58},[77,1738,1739],{"x":79,"y":949,"style":81,"fill":82,"textAnchor":83},"Not every failure deserves a second attempt",[53,1741],{"x":87,"y":953,"width":1742,"height":1743,"rx":134,"fill":171,"stroke":172,"style":137},"222","172",[77,1745,1749],{"x":1746,"y":1747,"style":1748,"fill":178,"textAnchor":83},"135","70","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","transient: retry",[77,1751,1753],{"x":1746,"y":1752,"style":98,"fill":75,"textAnchor":83},"100","timeout, reset",[77,1755,1756],{"x":1746,"y":177,"style":98,"fill":75,"textAnchor":83},"502 · 503 · 504",[77,1758,1760],{"x":1746,"y":1759,"style":147,"fill":92,"textAnchor":83},"176","wait 2 s, 4 s, 8 s",[53,1762],{"x":1763,"y":953,"width":1742,"height":1743,"rx":134,"fill":187,"stroke":188,"style":137},"269",[77,1765,1766],{"x":79,"y":1747,"style":1748,"fill":188,"textAnchor":83},"permanent: stop",[77,1768,1769],{"x":79,"y":1752,"style":98,"fill":75,"textAnchor":83},"400 · 401 · 403",[77,1771,1772],{"x":79,"y":177,"style":98,"fill":75,"textAnchor":83},"404",[77,1774,1775],{"x":79,"y":1759,"style":147,"fill":92,"textAnchor":83},"same answer every time",[53,1777],{"x":1778,"y":953,"width":1742,"height":1743,"rx":134,"fill":187,"stroke":1779,"style":137},"514","#b45309",[77,1781,1783],{"x":1782,"y":1747,"style":1748,"fill":1779,"textAnchor":83},"625","configuration: fix",[77,1785,1786],{"x":1782,"y":1752,"style":98,"fill":75,"textAnchor":83},"SSL handshake",[77,1788,1789],{"x":1782,"y":177,"style":98,"fill":75,"textAnchor":83},"407 proxy auth",[77,1791,1792],{"x":1782,"y":1759,"style":147,"fill":92,"textAnchor":83},"change settings, not code",[200,1794,1796],{"id":1795},"timeouts-and-retries","Timeouts and retries",[14,1798,1799,1800,1803],{},"QGIS applies one network timeout to every request, set in ",[689,1801,1802],{},"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.",[237,1805,1807],{"className":239,"code":1806,"language":241,"meta":242,"style":242},"import time\nfrom qgis.core import QgsNetworkAccessManager\n\nQgsNetworkAccessManager.setTimeout(120_000)\n\nTRANSIENT = {502, 503, 504}\n\ndef get_json_with_retry(url, params=None, attempts=4):\n    for attempt in range(attempts):\n        try:\n            return get_json(url, params)\n        except RuntimeError as exc:\n            status = str(exc).split(\":\", 1)[0].replace(\"HTTP \", \"\")\n            retryable = status in {str(s) for s in TRANSIENT} or \"timed out\" in str(exc).lower()\n            if not retryable or attempt == attempts - 1:\n                raise\n            time.sleep(2 ** (attempt + 1))\n",[18,1808,1809,1816,1827,1831,1841,1845,1872,1876,1898,1914,1920,1928,1941,1978,2025,2052,2057],{"__ignoreMap":242},[246,1810,1811,1813],{"class":115,"line":248},[246,1812,252],{"class":251},[246,1814,1815],{"class":255}," time\n",[246,1817,1818,1820,1822,1824],{"class":115,"line":259},[246,1819,262],{"class":251},[246,1821,291],{"class":255},[246,1823,252],{"class":251},[246,1825,1826],{"class":255}," QgsNetworkAccessManager\n",[246,1828,1829],{"class":115,"line":273},[246,1830,303],{"emptyLinePlaceholder":302},[246,1832,1833,1836,1839],{"class":115,"line":286},[246,1834,1835],{"class":255},"QgsNetworkAccessManager.setTimeout(",[246,1837,1838],{"class":322},"120_000",[246,1840,444],{"class":255},[246,1842,1843],{"class":115,"line":299},[246,1844,303],{"emptyLinePlaceholder":302},[246,1846,1847,1850,1853,1856,1859,1861,1864,1866,1869],{"class":115,"line":306},[246,1848,1849],{"class":322},"TRANSIENT",[246,1851,1852],{"class":251}," =",[246,1854,1855],{"class":255}," {",[246,1857,1858],{"class":322},"502",[246,1860,436],{"class":255},[246,1862,1863],{"class":322},"503",[246,1865,436],{"class":255},[246,1867,1868],{"class":322},"504",[246,1870,1871],{"class":255},"}\n",[246,1873,1874],{"class":115,"line":343},[246,1875,303],{"emptyLinePlaceholder":302},[246,1877,1878,1880,1883,1885,1887,1889,1892,1894,1896],{"class":115,"line":354},[246,1879,309],{"class":251},[246,1881,1882],{"class":312}," get_json_with_retry",[246,1884,316],{"class":255},[246,1886,319],{"class":251},[246,1888,323],{"class":322},[246,1890,1891],{"class":255},", attempts",[246,1893,319],{"class":251},[246,1895,975],{"class":322},[246,1897,340],{"class":255},[246,1899,1900,1903,1906,1908,1911],{"class":115,"line":363},[246,1901,1902],{"class":251},"    for",[246,1904,1905],{"class":255}," attempt ",[246,1907,383],{"class":251},[246,1909,1910],{"class":322}," range",[246,1912,1913],{"class":255},"(attempts):\n",[246,1915,1916,1918],{"class":115,"line":374},[246,1917,1270],{"class":251},[246,1919,1079],{"class":255},[246,1921,1922,1925],{"class":115,"line":389},[246,1923,1924],{"class":251},"            return",[246,1926,1927],{"class":255}," get_json(url, params)\n",[246,1929,1930,1933,1935,1938],{"class":115,"line":401},[246,1931,1932],{"class":251},"        except",[246,1934,541],{"class":322},[246,1936,1937],{"class":251}," as",[246,1939,1940],{"class":255}," exc:\n",[246,1942,1943,1946,1948,1951,1954,1957,1959,1961,1964,1966,1969,1972,1974,1976],{"class":115,"line":407},[246,1944,1945],{"class":255},"            status ",[246,1947,319],{"class":251},[246,1949,1950],{"class":322}," str",[246,1952,1953],{"class":255},"(exc).split(",[246,1955,1956],{"class":432},"\":\"",[246,1958,436],{"class":255},[246,1960,1591],{"class":322},[246,1962,1963],{"class":255},")[",[246,1965,55],{"class":322},[246,1967,1968],{"class":255},"].replace(",[246,1970,1971],{"class":432},"\"HTTP \"",[246,1973,436],{"class":255},[246,1975,1515],{"class":432},[246,1977,444],{"class":255},[246,1979,1980,1983,1985,1988,1990,1992,1994,1997,2000,2003,2005,2008,2011,2014,2017,2020,2022],{"class":115,"line":412},[246,1981,1982],{"class":255},"            retryable ",[246,1984,319],{"class":251},[246,1986,1987],{"class":255}," status ",[246,1989,383],{"class":251},[246,1991,1855],{"class":255},[246,1993,395],{"class":322},[246,1995,1996],{"class":255},"(s) ",[246,1998,1999],{"class":251},"for",[246,2001,2002],{"class":255}," s ",[246,2004,383],{"class":251},[246,2006,2007],{"class":322}," TRANSIENT",[246,2009,2010],{"class":255},"} ",[246,2012,2013],{"class":251},"or",[246,2015,2016],{"class":432}," \"timed out\"",[246,2018,2019],{"class":251}," in",[246,2021,1950],{"class":322},[246,2023,2024],{"class":255},"(exc).lower()\n",[246,2026,2027,2029,2031,2034,2036,2038,2041,2044,2047,2050],{"class":115,"line":423},[246,2028,1277],{"class":251},[246,2030,1220],{"class":251},[246,2032,2033],{"class":255}," retryable ",[246,2035,2013],{"class":251},[246,2037,1905],{"class":255},[246,2039,2040],{"class":251},"==",[246,2042,2043],{"class":255}," attempts ",[246,2045,2046],{"class":251},"-",[246,2048,2049],{"class":322}," 1",[246,2051,1079],{"class":255},[246,2053,2054],{"class":115,"line":447},[246,2055,2056],{"class":251},"                raise\n",[246,2058,2059,2062,2065,2068,2071,2074,2076],{"class":115,"line":452},[246,2060,2061],{"class":255},"            time.sleep(",[246,2063,2064],{"class":322},"2",[246,2066,2067],{"class":251}," **",[246,2069,2070],{"class":255}," (attempt ",[246,2072,2073],{"class":251},"+",[246,2075,2049],{"class":322},[246,2077,595],{"class":255},[14,2079,2080,675,2082,2085,2086,2089],{},[211,2081,674],{},[18,2083,2084],{},"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 ",[18,2087,2088],{},"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.",[200,2091,2093],{"id":2092},"qgis-version-compatibility","QGIS version compatibility",[14,2095,2096,2098,2099,2101,2102,436,2105,436,2108,2111,2112,1451,2115,2118,2119,2122],{},[18,2097,234],{}," arrived in QGIS 3.6 and ",[18,2100,1468],{}," in 3.2; both are unchanged in 3.40 and 3.44. On the QGIS 4 series, PyQt6 requires scoped enums: ",[18,2103,2104],{},"QNetworkRequest.Attribute.HttpStatusCodeAttribute",[18,2106,2107],{},"QNetworkRequest.KnownHeaders.ContentTypeHeader",[18,2109,2110],{},"QNetworkReply.NetworkError.NoError",", and ",[18,2113,2114],{},"QgsBlockingNetworkRequest.ErrorCode.NoError",[18,2116,2117],{},"QEventLoop.exec()"," replaces ",[18,2120,2121],{},"exec_()",". Writing the scoped forms now keeps the code working on both.",[200,2124,2126],{"id":2125},"troubleshooting","Troubleshooting",[205,2128,2129,2138,2144,2150,2159],{},[208,2130,2131,2137],{},[211,2132,2133,2134,2136],{},"Works with ",[18,2135,20],{}," on your machine, fails in QGIS for users."," Their proxy or CA is configured in QGIS; use the QGIS stack everywhere.",[208,2139,2140,2143],{},[211,2141,2142],{},"The reply handler never runs."," The object holding the reply was garbage-collected; keep a reference.",[208,2145,2146,2149],{},[211,2147,2148],{},"QGIS freezes during a request."," A blocking request is running in UI code; switch to the asynchronous pattern.",[208,2151,2152,2155,2156,225],{},[211,2153,2154],{},"Stale data comes back."," The response was cached; force a refresh or send ",[18,2157,2158],{},"Cache-Control: no-cache",[208,2160,2161,2164],{},[211,2162,2163],{},"SSL errors on an internal server."," Add the organisation's CA certificate in QGIS's authentication settings.",[200,2166,2168],{"id":2167},"conclusion","Conclusion",[14,2170,2171,2172,2174,2175,2177,2178,2180],{},"Route every HTTP call through QGIS's network stack so proxies, certificates and stored credentials apply. Use ",[18,2173,234],{}," in scripts and algorithms, the shared manager with a ",[18,2176,1027],{}," handler in plugin UI code, and ",[18,2179,1468],{}," for files. Attach credentials with authentication configs, and retry only the failures that are genuinely transient.",[200,2182,2184],{"id":2183},"frequently-asked-questions","Frequently Asked Questions",[14,2186,2187,2193],{},[211,2188,2189,2190,2192],{},"Can I still use ",[18,2191,20],{}," in a plugin?","\nYou 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.",[14,2195,2196,2199,2202],{},[211,2197,2198],{},"How do I send custom headers such as an API key?",[18,2200,2201],{},"request.setRawHeader(b\"X-API-Key\", key.encode())"," — or better, store the key in an authentication configuration using the API header method.",[14,2204,2205,2208,2209,2211,2212,2215,2216,225],{},[211,2206,2207],{},"Can a QgsTask use these requests?","\nYes. Use ",[18,2210,234],{}," inside ",[18,2213,2214],{},"run()","; it is safe in the task thread, as in ",[30,2217,2219],{"href":2218},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Frun-background-task-with-qgstask-pyqgis\u002F","running a long task with QgsTask",[14,2221,2222,2225,2226,2229],{},[211,2223,2224],{},"How do I see what QGIS actually sent?","\nOpen the ",[689,2227,2228],{},"Network Logger"," in the developer tools panel; every request through the manager appears there with headers and timings.",[200,2231,2233],{"id":2232},"related","Related",[205,2235,2236,2241,2247,2253,2258],{},[208,2237,2238,2240],{},[30,2239,33],{"href":32}," — the guide this recipe belongs to",[208,2242,2243],{},[30,2244,2246],{"href":2245},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fload-arcgis-rest-feature-service-pyqgis\u002F","Load an ArcGIS REST Feature Service in PyQGIS",[208,2248,2249],{},[30,2250,2252],{"href":2251},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fload-ogc-api-features-layer-pyqgis\u002F","Load an OGC API Features Layer in PyQGIS",[208,2254,2255],{},[30,2256,2257],{"href":223},"Store Credentials with QgsAuthManager in PyQGIS",[208,2259,2260],{},[30,2261,2263],{"href":2262},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fshow-plugin-progress-and-cancellation-pyqgis\u002F","Show Progress and Support Cancellation in a QGIS Plugin",[2265,2266,2267],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":242,"searchDepth":259,"depth":259,"links":2269},[2270,2271,2272,2273,2274,2275,2276,2277,2278,2279,2280],{"id":202,"depth":259,"text":203},{"id":228,"depth":259,"text":229},{"id":719,"depth":259,"text":720},{"id":1033,"depth":259,"text":1034},{"id":1462,"depth":259,"text":1463},{"id":1795,"depth":259,"text":1796},{"id":2092,"depth":259,"text":2093},{"id":2125,"depth":259,"text":2126},{"id":2167,"depth":259,"text":2168},{"id":2183,"depth":259,"text":2184},{"id":2232,"depth":259,"text":2233},"Call web APIs from PyQGIS through QGIS's own network stack — blocking requests for scripts, asynchronous requests that keep plugins responsive, file downloads with progress, authentication configs, proxies, timeouts and JSON POST bodies.","md",{"slug":2284,"type":2285,"breadcrumb":2286,"datePublished":2287,"dateModified":2287},"make-http-requests-with-qgsnetworkaccessmanager-pyqgis","article","HTTP Requests with QgsNetworkAccessManager","2026-09-17","\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fmake-http-requests-with-qgsnetworkaccessmanager-pyqgis",{"title":5,"description":2281},"spatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fmake-http-requests-with-qgsnetworkaccessmanager-pyqgis\u002Findex","77C5zjb4G91SQo9vpCqMAoASUBuO4YpXjpyT0UJaKJY",1789632908668]