Skip to content

Commit e0e27d9

Browse files
committed
[Bugfix] workaround for the issue when QGIS server timed out when requesting the legend
If the request failed, try to get the legend graphic for each layer separately This is a workaround for the issue when QGIS server timed out when requesting the legend graphic for multiple layers at once (LAYER parameter with multiple values) Related to #4521 Funded by CédéGIS https://www.cedegis.fr/
1 parent 97e20ea commit e0e27d9

File tree

2 files changed

+206
-10
lines changed

2 files changed

+206
-10
lines changed

assets/src/modules/action/Symbology.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* @license MPL-2.0
77
*/
88

9+
import { HttpError } from '../Errors.js';
910
import WMS from './../WMS.js';
1011
import {LayerTreeLayerState, LayerTreeGroupState} from './../state/LayerTree.js'
1112

@@ -43,7 +44,28 @@ export async function updateLayerTreeLayersSymbology(treeLayers) {
4344
treeLayersByName[node.name].symbology = node;
4445
}
4546
}
46-
}).catch(console.error);
47+
}).catch((error) => {
48+
console.error(error);
49+
// If the request failed, try to get the legend graphic for each layer separately
50+
// This is a workaround for the issue when QGIS server timed out when requesting
51+
// the legend graphic for multiple layers at once (LAYER parameter with multiple values)
52+
if (treeLayers.length == 1) {
53+
// If there is only one layer, there is no need to try to get the legend graphic
54+
// for each layer separately
55+
return treeLayers;
56+
}
57+
if (!(error instanceof HttpError) || error.statusCode != 504) {
58+
// If the error is not a timeout, there is no need to try to get the legend graphic
59+
// for each layer separately
60+
return treeLayers;
61+
}
62+
// Try to get the legend graphic for each layer separately
63+
Promise.all(
64+
treeLayers.map(treeLayer => updateLayerTreeLayerSymbology(treeLayer))
65+
).then((treeLayers) => {
66+
return treeLayers;
67+
});
68+
});
4769
return treeLayers;
4870
}
4971

@@ -66,7 +88,9 @@ export async function updateLayerTreeLayerSymbology(treeLayer) {
6688
*/
6789
export async function updateLayerTreeGroupLayersSymbology(treeGroup) {
6890
if (!(treeGroup instanceof LayerTreeGroupState)) {
69-
throw new TypeError('`updateLayerTreeGroupLayersSymbology` method required a LayerTreeGroupState as parameter!');
91+
throw new TypeError(
92+
'`updateLayerTreeGroupLayersSymbology` method required a LayerTreeGroupState as parameter!'
93+
);
7094
}
7195
return updateLayerTreeLayersSymbology(treeGroup.findTreeLayers());
7296
}

tests/end2end/playwright/treeview.spec.js

Lines changed: 180 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,24 @@ test.describe('Treeview', () => {
1111
// Wait for WMS GetCapabilities promise
1212
let getCapabilitiesWMSPromise = page.waitForRequest(/SERVICE=WMS&REQUEST=GetCapabilities/);
1313
// Wait for WMS GetLegendGraphic promise
14-
const getLegendGraphicPromise = page.waitForRequest(request => request.method() === 'POST' && request.postData() != null && request.postData()?.includes('GetLegendGraphic') === true);
14+
const getLegendGraphicPromise = page.waitForRequest(
15+
request => request.method() === 'POST' &&
16+
request.postData() != null &&
17+
request.postData()?.includes('GetLegendGraphic') === true
18+
);
1519
await page.goto(url);
1620
// Wait for WMS GetCapabilities
1721
await getCapabilitiesWMSPromise;
1822
// Wait for WMS GetLegendGraphic
1923
let getLegendGraphicRequest = await getLegendGraphicPromise;
2024

2125
// Check WMS GetLegendGraphic postData
22-
const getLegendGraphicRequestPostData = getLegendGraphicRequest.postData();
23-
expect(getLegendGraphicRequestPostData).toContain('SERVICE=WMS')
24-
expect(getLegendGraphicRequestPostData).toContain('REQUEST=GetLegendGraphic')
25-
expect(getLegendGraphicRequestPostData).toContain('LAYER=sousquartiers%2Cquartiers%2Cshop_bakery_pg%2Ctramway_lines%2Cgroup_as_layer_1%2Cgroup_as_layer_2')
26+
const searchParams = new URLSearchParams(getLegendGraphicRequest.postData() ?? '');
27+
expect(searchParams.get('SERVICE')).toBe('WMS');
28+
expect(searchParams.get('REQUEST')).toBe('GetLegendGraphic');
29+
expect(searchParams.get('LAYER')).toBe(
30+
'sousquartiers,quartiers,shop_bakery_pg,tramway_lines,group_as_layer_1,group_as_layer_2'
31+
);
2632

2733
// Check that the map scale is the right one
2834
await expect(page.locator('#overview-bar .ol-scale-text')).toHaveText('1 : ' + (100180).toLocaleString(locale))
@@ -40,8 +46,12 @@ test.describe('Treeview', () => {
4046
await expect(page.getByTestId('sub-group1').getByTestId('subdistricts')).toHaveCount(1)
4147
await expect(page.getByTestId('subdistricts')).toBeVisible()
4248
await expect(page.getByTestId('group with space in name and shortname defined')).toHaveCount(1)
43-
await expect(page.getByTestId('group with space in name and shortname defined').getByTestId('quartiers')).toHaveCount(1)
44-
await expect(page.getByTestId('group with space in name and shortname defined').getByTestId('shop_bakery_pg')).toHaveCount(1)
49+
await expect(
50+
page.getByTestId('group with space in name and shortname defined').getByTestId('quartiers')
51+
).toHaveCount(1)
52+
await expect(
53+
page.getByTestId('group with space in name and shortname defined').getByTestId('shop_bakery_pg')
54+
).toHaveCount(1)
4555
await expect(page.getByTestId('tramway_lines')).toHaveCount(1)
4656
await expect(page.getByTestId('tramway_lines')).toHaveText('Tramway lines')
4757
await expect(page.getByTestId('group-without-children')).toHaveCount(0)
@@ -169,7 +179,8 @@ test.describe('Treeview', () => {
169179
});
170180
});
171181

172-
test.describe('Treeview mocked with "Hide checkboxes for groups" option', () => {
182+
test.describe('Treeview mocked', () => {
183+
173184
test('"Hide checkboxes for groups" option', async ({ page }) => {
174185
await page.route('**/service/getProjectConfig*', async route => {
175186
const response = await route.fetch();
@@ -183,4 +194,165 @@ test.describe('Treeview mocked with "Hide checkboxes for groups" option', () =>
183194

184195
await expect(page.locator('lizmap-treeview div.group > input')).toHaveCount(0);
185196
});
197+
198+
test('Timeout on GetLegendGraphic with multi layers', async ({ page }) => {
199+
const timedOutRequest = [];
200+
const GetLegends = [];
201+
await page.route('**/service*', async route => {
202+
const request = await route.request();
203+
if (request.method() !== 'POST') {
204+
// GetLegendGraphic is a POST request
205+
// Continue the request for non POST requests
206+
await route.continue();
207+
return;
208+
}
209+
const searchParams = new URLSearchParams(request.postData() ?? '');
210+
if (searchParams.get('SERVICE') !== 'WMS' ||
211+
!searchParams.has('REQUEST') ||
212+
searchParams.get('REQUEST') !== 'GetLegendGraphic') {
213+
// Continue the request for non GetLegendGraphic requests
214+
await route.continue();
215+
return;
216+
}
217+
if (!searchParams.has('LAYER')) {
218+
// Continue the request for GetLegendGraphic without LAYER parameter
219+
await route.continue();
220+
return;
221+
}
222+
const layers = searchParams.get('LAYER')?.split(',');
223+
if (layers?.length == 1) {
224+
// Continue the request for GetLegendGraphic with one layer
225+
GetLegends.push(searchParams);
226+
await route.continue();
227+
return;
228+
}
229+
timedOutRequest.push(searchParams);
230+
// Timeout on GetLegendGraphic with multi layers
231+
await route.fulfill({
232+
status: 504,
233+
contentType: 'text/plain',
234+
body: 'Timeout',
235+
});
236+
});
237+
const url = '/index.php/view/map/?repository=testsrepository&project=treeview';
238+
// Wait for WMS GetCapabilities promise
239+
let getCapabilitiesWMSPromise = page.waitForRequest(/SERVICE=WMS&REQUEST=GetCapabilities/);
240+
241+
await page.goto(url);
242+
243+
// Wait for WMS GetCapabilities
244+
await getCapabilitiesWMSPromise;
245+
246+
let timeCount = 0;
247+
while (GetLegends.length < 6) {
248+
timeCount += 100;
249+
if (timeCount > 1000) {
250+
break;
251+
}
252+
await page.waitForTimeout(100);
253+
}
254+
255+
await expect(GetLegends.length).toBeGreaterThanOrEqual(6);
256+
await expect(timedOutRequest.length).toBeGreaterThanOrEqual(1);
257+
258+
// Check that the GetLegendGraphic requests are well formed
259+
GetLegends.forEach((searchParams) => {
260+
expect(searchParams.get('SERVICE')).toBe('WMS');
261+
expect(searchParams.get('REQUEST')).toBe('GetLegendGraphic');
262+
expect(searchParams.get('VERSION')).toBe('1.3.0');
263+
expect(searchParams.get('FORMAT')).toBe('application/json');
264+
expect(searchParams.get('LAYER')).toBeDefined();
265+
expect(searchParams.get('LAYER')).not.toContain(',');
266+
expect(searchParams.get('STYLES')).toBeDefined();
267+
expect(searchParams.get('STYLES')).not.toContain(',');
268+
});
269+
270+
// Check that the timed out GetLegendGraphic requests are well formed
271+
timedOutRequest.forEach((searchParams) => {
272+
expect(searchParams.get('SERVICE')).toBe('WMS');
273+
expect(searchParams.get('REQUEST')).toBe('GetLegendGraphic');
274+
expect(searchParams.get('VERSION')).toBe('1.3.0');
275+
expect(searchParams.get('FORMAT')).toBe('application/json');
276+
expect(searchParams.get('LAYER')).toBeDefined();
277+
expect(searchParams.get('LAYER')).toContain(',');
278+
expect(searchParams.get('STYLES')).toBeDefined();
279+
expect(searchParams.get('STYLES')).toContain(',');
280+
});
281+
282+
await page.unroute('**/service*');
283+
});
284+
285+
test('Error on GetLegendGraphic', async ({ page }) => {
286+
const abortedRequest = [];
287+
const GetLegends = [];
288+
await page.route('**/service*', async route => {
289+
const request = await route.request();
290+
if (request.method() !== 'POST') {
291+
// GetLegendGraphic is a POST request
292+
// Continue the request for non POST requests
293+
await route.continue();
294+
return;
295+
}
296+
const searchParams = new URLSearchParams(request.postData() ?? '');
297+
if (searchParams.get('SERVICE') !== 'WMS' ||
298+
!searchParams.has('REQUEST') ||
299+
searchParams.get('REQUEST') !== 'GetLegendGraphic') {
300+
// Continue the request for non GetLegendGraphic requests
301+
await route.continue();
302+
return;
303+
}
304+
if (!searchParams.has('LAYER')) {
305+
// Continue the request for GetLegendGraphic without LAYER parameter
306+
await route.continue();
307+
return;
308+
}
309+
const layers = searchParams.get('LAYER')?.split(',');
310+
if (layers?.length == 1) {
311+
// Continue the request for GetLegendGraphic with one layer
312+
GetLegends.push(searchParams);
313+
await route.continue();
314+
return;
315+
}
316+
abortedRequest.push(searchParams);
317+
// Abort the request for GetLegendGraphic with multiple layers
318+
await route.abort('failed');
319+
});
320+
321+
const url = '/index.php/view/map/?repository=testsrepository&project=treeview';
322+
// Wait for WMS GetCapabilities promise
323+
let getCapabilitiesWMSPromise = page.waitForRequest(/SERVICE=WMS&REQUEST=GetCapabilities/);
324+
325+
await page.goto(url);
326+
327+
// Wait for WMS GetCapabilities
328+
await getCapabilitiesWMSPromise;
329+
330+
// Wait for WMS GetLegendGraphic
331+
let timeCount = 0;
332+
while (GetLegends.length < 6) {
333+
timeCount += 100;
334+
if (timeCount > 200) {
335+
break;
336+
}
337+
await page.waitForTimeout(100);
338+
}
339+
340+
// Check if the GetLegendGraphic requests were all aborted
341+
await expect(GetLegends.length).toBe(0);
342+
await expect(abortedRequest.length).toBe(1);
343+
344+
// Check that the aborted GetLegendGraphic requests are well formed
345+
abortedRequest.forEach((searchParams) => {
346+
expect(searchParams.get('SERVICE')).toBe('WMS');
347+
expect(searchParams.get('REQUEST')).toBe('GetLegendGraphic');
348+
expect(searchParams.get('VERSION')).toBe('1.3.0');
349+
expect(searchParams.get('FORMAT')).toBe('application/json');
350+
expect(searchParams.get('LAYER')).toBeDefined();
351+
expect(searchParams.get('LAYER')).toContain(',');
352+
expect(searchParams.get('STYLES')).toBeDefined();
353+
expect(searchParams.get('STYLES')).toContain(',');
354+
});
355+
356+
await page.unroute('**/service*');
357+
});
186358
});

0 commit comments

Comments
 (0)