From 4eff0fb4973327b9a4887d2e548fd190de774fb0 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 25 Jun 2025 19:45:46 +0200 Subject: [PATCH] fix(webapp): catch possibe errors on request geolocation (#8640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * catch possibe errors on request geolocation * proper toast error * remove deprecated request package, use node fetch instead, set timeout --------- Co-authored-by: Max Co-authored-by: Wolfgang Huß --- backend/package.json | 1 - .../src/graphql/resolvers/users/location.ts | 161 +++++++++--------- webapp/components/Select/LocationSelect.vue | 25 ++- 3 files changed, 99 insertions(+), 88 deletions(-) diff --git a/backend/package.json b/backend/package.json index 38a2cddb1..ef2721261 100644 --- a/backend/package.json +++ b/backend/package.json @@ -83,7 +83,6 @@ "nodemailer-html-to-text": "^3.2.0", "preview-email": "^3.1.0", "pug": "^3.0.3", - "request": "~2.88.2", "sanitize-html": "~2.17.0", "slug": "~9.1.0", "trunc-html": "~1.1.2", diff --git a/backend/src/graphql/resolvers/users/location.ts b/backend/src/graphql/resolvers/users/location.ts index dc515e70d..4f72f2b9e 100644 --- a/backend/src/graphql/resolvers/users/location.ts +++ b/backend/src/graphql/resolvers/users/location.ts @@ -6,27 +6,15 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable promise/avoid-new */ -/* eslint-disable promise/prefer-await-to-callbacks */ +/* eslint-disable n/no-unsupported-features/node-builtins */ import { UserInputError } from 'apollo-server' -import request from 'request' import CONFIG from '@config/index' -const fetch = (url) => { - return new Promise((resolve, reject) => { - request(url, function (error, response, body) { - if (error) { - reject(error) - } else { - resolve(JSON.parse(body)) - } - }) - }) -} - const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl', 'ru'] +const REQUEST_TIMEOUT = 3000 + const createLocation = async (session, mapboxData) => { const data = { id: mapboxData.id + (mapboxData.address ? `-${mapboxData.address}` : ''), @@ -78,74 +66,80 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s let locationId - if (locationName !== null) { - const res: any = await fetch( - `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent( - locationName, - )}.json?access_token=${ - CONFIG.MAPBOX_TOKEN - }&types=region,place,country,address&language=${locales.join(',')}`, - ) + try { + if (locationName !== null) { + const response: any = await fetch( + `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent( + locationName, + )}.json?access_token=${ + CONFIG.MAPBOX_TOKEN + }&types=region,place,country,address&language=${locales.join(',')}`, + { + signal: AbortSignal.timeout(REQUEST_TIMEOUT), + }, + ) - if (!res?.features?.[0]) { - throw new UserInputError('locationName is invalid') - } + const res = await response.json() - let data - - res.features.forEach((item) => { - if (item.matching_place_name === locationName) { - data = item + if (!res?.features?.[0]) { + throw new UserInputError('locationName is invalid') } - }) - if (!data) { - data = res.features[0] - } - if (!data?.place_type?.length) { - throw new UserInputError('locationName is invalid') - } + let data - if (data.place_type.length > 1) { - data.id = 'region.' + data.id.split('.')[1] - } - await createLocation(session, data) + res.features.forEach((item) => { + if (item.matching_place_name === locationName) { + data = item + } + }) + if (!data) { + data = res.features[0] + } - let parent = data + if (!data?.place_type?.length) { + throw new UserInputError('locationName is invalid') + } - if (parent.address) { - parent.id += `-${parent.address}` - } + if (data.place_type.length > 1) { + data.id = 'region.' + data.id.split('.')[1] + } + await createLocation(session, data) - if (data.context) { - for await (const ctx of data.context) { - await createLocation(session, ctx) - await session.writeTransaction((transaction) => { - return transaction.run( - ` + let parent = data + + if (parent.address) { + parent.id += `-${parent.address}` + } + + if (data.context) { + for await (const ctx of data.context) { + await createLocation(session, ctx) + await session.writeTransaction((transaction) => { + return transaction.run( + ` MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) MERGE (child)<-[:IS_IN]-(parent) RETURN child.id, parent.id `, - { - parentId: parent.id, - childId: ctx.id, - }, - ) - }) - parent = ctx + { + parentId: parent.id, + childId: ctx.id, + }, + ) + }) + parent = ctx + } } + + locationId = data.id + } else { + locationId = 'non-existent-id' } - locationId = data.id - } else { - locationId = 'non-existent-id' - } - - // delete all current locations from node and add new location - await session.writeTransaction((transaction) => { - return transaction.run( - ` + // delete all current locations from node and add new location + await session.writeTransaction((transaction) => { + return transaction.run( + ` MATCH (node:${nodeLabel} {id: $nodeId}) OPTIONAL MATCH (node)-[relationship:IS_IN]->(:Location) DELETE relationship @@ -154,18 +148,29 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s MERGE (node)-[:IS_IN]->(location) RETURN location.id, node.id `, - { nodeId, locationId }, - ) - }) + { nodeId, locationId }, + ) + }) + } catch (error) { + throw new Error(error) + } } export const queryLocations = async ({ place, lang }) => { - const res: any = await fetch( - `https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${lang}`, - ) - // Return empty array if no location found or error occurred - if (!res?.features) { - return [] + try { + const res: any = await fetch( + `https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${lang}`, + { + signal: AbortSignal.timeout(REQUEST_TIMEOUT), + }, + ) + const response = await res.json() + // Return empty array if no location found or error occurred + if (!response?.features) { + return [] + } + return response.features + } catch (error) { + throw new Error(error) } - return res.features } diff --git a/webapp/components/Select/LocationSelect.vue b/webapp/components/Select/LocationSelect.vue index 9ea808aad..8a9dedf12 100644 --- a/webapp/components/Select/LocationSelect.vue +++ b/webapp/components/Select/LocationSelect.vue @@ -107,19 +107,26 @@ export default { this.cities = [] return } - this.loadingGeo = true - const place = encodeURIComponent(value) - const lang = this.$i18n.locale() + try { + this.loadingGeo = true - const { - data: { queryLocations: result }, - } = await this.$apollo.query({ query: queryLocations(), variables: { place, lang } }) + const place = encodeURIComponent(value) + const lang = this.$i18n.locale() - this.cities = this.processLocationsResult(result) - this.loadingGeo = false + const { + data: { queryLocations: result }, + } = await this.$apollo.query({ query: queryLocations(), variables: { place, lang } }) - return this.cities.find((city) => city.value === value) + this.cities = this.processLocationsResult(result) + this.loadingGeo = false + + return this.cities.find((city) => city.value === value) + } catch (error) { + this.$toast.error(error.message) + } finally { + this.loadingGeo = false + } }, clearLocationName(event) { event.target.value = ''