Skip to content

Commit 5ca0c09

Browse files
gadomskiPhil Varner
andauthored
Remove vestiages of geojsonhint (#556)
* fix: remove vestiages of geojsonhint * improve error handling * Update CHANGELOG.md --------- Co-authored-by: Phil Varner <[email protected]>
1 parent 92a5e57 commit 5ca0c09

File tree

7 files changed

+198
-103
lines changed

7 files changed

+198
-103
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Changed
11+
12+
- Simplify the error handling around geometry errors.
13+
- When an OpenSearch request returns a 400 status code, use this same status code with a meaningful error message in the stac-server response, instead of always returning a 500 error.
14+
815
## [2.2.2] - 2023-07-06
916

1017
### Changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -587,14 +587,16 @@ either be done through the OpenSearch API or Dashboard.
587587

588588
##### Option 1 - API method
589589

590-
This assumes the master username is `admin` and creats a user with the name `stac_server`.
590+
This assumes the master username is `admin` and creates a user with the name `stac_server`.
591+
Environment variables `HOST` and `OPENSEARCH_MASTER_USER_PASSWORD` should be set in the
592+
shell environment.
591593

592594
Create the Role:
593595

594596
```shell
595597
curl -X "PUT" "${HOST}/_plugins/_security/api/roles/stac_server_role" \
596598
-H 'Content-Type: application/json; charset=utf-8' \
597-
-u 'admin:xxxxxxxx' \
599+
-u "admin:${OPENSEARCH_MASTER_USER_PASSWORD}" \
598600
-d $'{
599601
"cluster_permissions": [
600602
"cluster_composite_ops",
@@ -629,7 +631,7 @@ Create the User:
629631
```shell
630632
curl -X "PUT" "${HOST}/_plugins/_security/api/internalusers/stac_server" \
631633
-H 'Content-Type: application/json; charset=utf-8' \
632-
-u 'admin:xxxxxxxx' \
634+
-u "admin:${OPENSEARCH_MASTER_USER_PASSWORD}" \
633635
-d $'{ "password": "xxx" }'
634636
```
635637

@@ -640,7 +642,7 @@ Map the Role to the User:
640642
```shell
641643
curl -X "PUT" "${HOST}/_plugins/_security/api/rolesmapping/stac_server_role" \
642644
-H 'Content-Type: application/json; charset=utf-8' \
643-
-u 'admin:xxxxxxxx' \
645+
-u "admin:${OPENSEARCH_MASTER_USER_PASSWORD}" \
644646
-d $'{
645647
"users": [
646648
"stac_server"

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
"dependencies": {
6464
"@aws-sdk/client-secrets-manager": "^3.359.0",
6565
"@mapbox/extent": "^0.4.0",
66-
"@mapbox/geojsonhint": "^3.2.0",
6766
"@opensearch-project/opensearch": "^2.2.1",
6867
"cors": "^2.8.5",
6968
"express": "^4.18.2",

src/lib/api.js

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -618,19 +618,6 @@ const searchItems = async function (collectionId, queryParameters, backend, endp
618618

619619
logger.info('Search parameters (processed): %j', searchParams)
620620

621-
// try { // Attempt to catch invalid geometry before querying Search
622-
// const hints = geometry ? geojsonhint.hint(geometry, {}) : []
623-
624-
// if (hints.length > 0) {
625-
// return {
626-
// statusCode: 400,
627-
// body: hints.map(({ message }) => ({ message }))
628-
// }
629-
// }
630-
// } catch (e) {
631-
// logger.error(e)
632-
// }
633-
634621
let esResponse
635622
try {
636623
esResponse = await backend.search(searchParams, page, limit)
@@ -644,29 +631,24 @@ const searchItems = async function (collectionId, queryParameters, backend, endp
644631
},
645632
results: []
646633
}
647-
} else {
648-
try {
649-
// @ts-ignore
650-
const e = error['meta']['body']['error']
651-
652-
let errorMessage
653-
if ('caused_by' in e) { // Parse certain types of geometry errors from Search
654-
errorMessage = e['caused_by'].map(({ message }) => ({ message }))
655-
} else if ('root_cause' in e) { // Parse other types of geometry errors from Search
656-
errorMessage = e['root_cause'].map(({ reason }) => ({ reason }))
657-
} else if (JSON.stringify(error).includes('failed to create query')) {
658-
errorMessage = 'Query failed. Please verify a valid query payload.'
659-
} else {
660-
throw error
661-
}
662-
663-
return {
664-
statusCode: 400,
665-
body: errorMessage
666-
}
667-
} catch (_) {
668-
throw error
634+
// @ts-ignore
635+
} else if (error?.meta?.statusCode === 400) {
636+
// @ts-ignore
637+
const e = error?.meta?.body?.error
638+
639+
// only serialize part of the error message,
640+
// as error.meta.meta.connection will leak the OpenSearch URL
641+
let errorMessage
642+
if ('caused_by' in e) {
643+
errorMessage = JSON.stringify(e?.caused_by?.reason)
644+
} else if (JSON.stringify(e).includes('failed to create query')) {
645+
errorMessage = `Query failed with invalid parameters: ${JSON.stringify(e)}`
646+
} else {
647+
errorMessage = `Unknown error: ${JSON.stringify(e)}`
669648
}
649+
throw new ValidationError(errorMessage)
650+
} else {
651+
throw error
670652
}
671653
}
672654

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"type": "MultiPolygon",
3+
"coordinates": [
4+
[
5+
[
6+
[
7+
100.0,
8+
0.0
9+
],
10+
[
11+
101.0,
12+
0.0
13+
],
14+
[
15+
101.0,
16+
1.0
17+
],
18+
[
19+
100.0,
20+
1.0
21+
],
22+
[
23+
100.0,
24+
2.0
25+
],
26+
[
27+
100.0,
28+
1.0
29+
],
30+
[
31+
100.0,
32+
0.0
33+
]
34+
]
35+
],
36+
[
37+
[
38+
[
39+
100.0,
40+
0.0
41+
],
42+
[
43+
101.0,
44+
0.0
45+
],
46+
[
47+
101.0,
48+
1.0
49+
],
50+
[
51+
100.0,
52+
1.0
53+
],
54+
[
55+
100.0,
56+
2.0
57+
],
58+
[
59+
100.0,
60+
1.0
61+
],
62+
[
63+
100.0,
64+
0.0
65+
]
66+
]
67+
]
68+
]
69+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"type": "Polygon",
3+
"coordinates": [
4+
[
5+
[100.0, 0.0],
6+
[100.0, 1.0],
7+
[101.0, 1.0],
8+
[101.0, 0.0],
9+
[100.0, 0.0]
10+
]
11+
]
12+
}

tests/system/test-api-search-post.js

Lines changed: 87 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const __filename = fileURLToPath(import.meta.url)
1414
const __dirname = path.dirname(__filename) // eslint-disable-line no-unused-vars
1515
const intersectsGeometry = fs.readFileSync(path.resolve(__dirname, '../fixtures/stac/intersectsGeometry.json'), 'utf8')
1616

17-
// const fixture = (filepath) => fs.readFileSync(path.resolve(__dirname, filepath), 'utf8')
17+
const fixture = (filepath) => fs.readFileSync(path.resolve(__dirname, filepath), 'utf8')
1818

1919
const ingestEntities = async (fixtures) => {
2020
await ingestItems(
@@ -420,68 +420,92 @@ test('/search preserve intersects geometry in next link', async (t) => {
420420
t.deepEqual(nextLink.body.intersects, intersectsGeometry)
421421
})
422422

423-
// test('POST /search using bad geometry, expecting useful error messages', async (t) => {
424-
// let response = null
425-
426-
// response = await t.context.api.client.post('search', {
427-
// json: {
428-
// intersects: fixture('../fixtures/geometry/badGeoUnclosed.json')
429-
// }
430-
// })
431-
// t.is(response.statusCode, 400)
432-
// t.deepEqual(response.body,
433-
// [{ message: 'the first and last positions in a LinearRing of coordinates must be the same' }])
434-
435-
// response = await t.context.api.client.post('search', {
436-
// json: {
437-
// intersects: fixture('../fixtures/geometry/badGeoRightHandRule.json')
438-
// }
439-
// })
440-
// t.is(response.statusCode, 400)
441-
// t.deepEqual(response.body,
442-
// [{ message: 'Polygons and MultiPolygons should follow the right-hand rule' }])
443-
444-
// response = await t.context.api.client.post('search', {
445-
// json: {
446-
// intersects: fixture('../fixtures/geometry/badGeoFourPoints.json')
447-
// }
448-
// })
449-
// t.is(response.statusCode, 400)
450-
// t.deepEqual(response.body, [
451-
// {
452-
// reason: 'failed to create query: at least 4 polygon points required'
453-
// },
454-
// {
455-
// reason: 'failed to create query: at least 4 polygon points required'
456-
// }
457-
// ])
458-
459-
// response = await t.context.api.client.post('search', {
460-
// json: {
461-
// intersects: fixture('../fixtures/geometry/badGeoDuplicateConsecutive.json')
462-
// }
463-
// })
464-
// t.is(response.statusCode, 400)
465-
// t.deepEqual(response.body, [
466-
// {
467-
// reason: 'failed to create query:
468-
// Provided shape has duplicate consecutive coordinates at: (POINT (100.0 1.0))'
469-
// },
470-
// {
471-
// reason: 'failed to create query:
472-
// Provided shape has duplicate consecutive coordinates at: (POINT (100.0 1.0))'
473-
// }
474-
// ])
475-
476-
// response = await t.context.api.client.post('search', {
477-
// json: {
478-
// intersects: fixture('../fixtures/geometry/badGeoRightHandRule2.json')
479-
// }
480-
// })
481-
// t.is(response.statusCode, 400)
482-
// t.deepEqual(response.body,
483-
// [{ message: 'Polygons and MultiPolygons should follow the right-hand rule' }])
484-
// })
423+
test('POST /search - polygon wound incorrectly, but should succeeed', async (t) => {
424+
const response = await t.context.api.client.post('search', {
425+
json: {
426+
intersects: fixture('../fixtures/geometry/polygonWoundCCW.json')
427+
}
428+
})
429+
t.is(response.features.length, 0)
430+
})
431+
432+
test('POST /search - failure when polygon is unclosed', async (t) => {
433+
const error = await t.throwsAsync(async () => t.context.api.client.post('search', {
434+
json: {
435+
intersects: fixture('../fixtures/geometry/badGeoUnclosed.json')
436+
}
437+
}))
438+
t.is(error.response.statusCode, 400)
439+
t.is(error.response.body.code, 'BadRequest')
440+
t.regex(error.response.body.description,
441+
/.*invalid LinearRing found \(coordinates are not closed\).*/)
442+
})
443+
444+
test('POST /search - failure when ambigous winding', async (t) => {
445+
// The right-hand rule part is ok (see:
446+
// https://github.com/stac-utils/stac-server/issues/549) but there's
447+
// coinciding points.
448+
const error = await t.throwsAsync(async () => t.context.api.client.post('search', {
449+
json: {
450+
intersects: fixture('../fixtures/geometry/badGeoRightHandRule.json')
451+
}
452+
}))
453+
t.is(error.response.statusCode, 400)
454+
t.is(error.response.body.code, 'BadRequest')
455+
t.regex(error.response.body.description,
456+
/.*failed to create query: Cannot determine orientation: edges adjacent to.*/)
457+
})
458+
459+
test('POST /search - failure when ambigous winding 2', async (t) => {
460+
// The right-hand rule part is ok (see:
461+
// https://github.com/stac-utils/stac-server/issues/549) but there's
462+
// coinciding points.
463+
const error = await t.throwsAsync(async () => t.context.api.client.post('search', {
464+
json: {
465+
intersects: fixture('../fixtures/geometry/badGeoRightHandRule2.json')
466+
}
467+
}))
468+
t.is(error.response.statusCode, 400)
469+
t.is(error.response.body.code, 'BadRequest')
470+
t.regex(error.response.body.description,
471+
/.*failed to create query: Cannot determine orientation: edges adjacent to.*/)
472+
})
473+
474+
test('POST /search - failure when Polygon only has 4 points ', async (t) => {
475+
const error = await t.throwsAsync(async () => t.context.api.client.post('search', {
476+
json: {
477+
intersects: fixture('../fixtures/geometry/badGeoFourPoints.json')
478+
}
479+
}))
480+
t.is(error.response.statusCode, 400)
481+
t.is(error.response.body.code, 'BadRequest')
482+
t.regex(error.response.body.description,
483+
/.*failed to create query: at least 4 polygon points required.*/)
484+
})
485+
486+
test('POST /search - failure when shape has duplicate consecutive coordinates', async (t) => {
487+
const error = await t.throwsAsync(async () => t.context.api.client.post('search', {
488+
json: {
489+
intersects: fixture('../fixtures/geometry/badGeoDuplicateConsecutive.json')
490+
}
491+
}))
492+
t.is(error.response.statusCode, 400)
493+
t.is(error.response.body.code, 'BadRequest')
494+
t.regex(error.response.body.description,
495+
/.*Provided shape has duplicate consecutive coordinates at.*/)
496+
})
497+
498+
test('POST /search - failure when MultiPolygon has only 4 points', async (t) => {
499+
const error = await t.throwsAsync(async () => t.context.api.client.post('search', {
500+
json: {
501+
intersects: fixture('../fixtures/geometry/badGeoFourPointsMultiPolygon.json')
502+
}
503+
}))
504+
t.is(error.response.statusCode, 400)
505+
t.is(error.response.body.code, 'BadRequest')
506+
t.regex(error.response.body.description,
507+
/.*failed to create query: at least 4 polygon points required.*/)
508+
})
485509

486510
test('/search preserve bbox in prev and next links', async (t) => {
487511
const bbox = [-180, -90, 180, 90]

0 commit comments

Comments
 (0)