From b44cd4502cac4a984e7a6594f701265454152875 Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Thu, 6 Jun 2019 10:46:24 -0400 Subject: [PATCH] Overhaul error handling --- routes/weather.ts | 29 ++++++++++++++++++++++++----- routes/weatherProviders/DarkSky.ts | 14 +++++++------- routes/weatherProviders/OWM.ts | 12 ++++++------ types.ts | 4 ++-- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/routes/weather.ts b/routes/weather.ts index 70450aa..48d62b1 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -71,7 +71,8 @@ async function resolveCoordinates( location: string ): Promise< GeoCoordinates > * Makes an HTTP/HTTPS GET request to the specified URL and parses the JSON response body. * @param url The URL to fetch. * @return A Promise that will be resolved the with parsed response body if the request succeeds, or will be rejected - * with an Error if the request or JSON parsing fails. + * with an error if the request or JSON parsing fails. This error may contain information about the HTTP request or, + * response including API keys and other sensitive information. */ export async function httpJSONRequest(url: string ): Promise< any > { try { @@ -110,6 +111,7 @@ function getTimeData( coordinates: GeoCoordinates ): TimeData { * @param adjustmentOptions Options to tweak the calculation, or undefined/null if no custom values are to be used. * @param wateringData The weather to use to calculate watering percentage. * @return The percentage that watering should be scaled by. + * @throws An error message will be thrown if the watering scale cannot be calculated. */ function calculateZimmermanWateringScale( adjustmentOptions: AdjustmentOptions, wateringData: WateringData ): number { @@ -117,7 +119,7 @@ function calculateZimmermanWateringScale( adjustmentOptions: AdjustmentOptions, // Check to make sure valid data exists for all factors if ( !validateValues( [ "temp", "humidity", "precip" ], wateringData ) ) { - return 100; + throw "Necessary field(s) were missing from WateringData."; } // Get baseline conditions for 100% water level, if provided @@ -196,7 +198,13 @@ export const getWeatherData = async function( req: express.Request, res: express // Continue with the weather request const timeData: TimeData = getTimeData( coordinates ); - const weatherData: WeatherData = await weatherProvider.getWeatherData( coordinates ); + let weatherData: WeatherData; + try { + weatherData = await weatherProvider.getWeatherData( coordinates ); + } catch ( err ) { + res.send( "Error: " + err ); + return; + } res.json( { ...timeData, @@ -259,7 +267,12 @@ export const getWateringData = async function( req: express.Request, res: expres return; } - wateringData = await weatherProvider.getWateringData( coordinates ); + try { + wateringData = await weatherProvider.getWateringData( coordinates ); + } catch ( err ) { + res.send( "Error: " + err ); + return; + } } let scale = -1, rainDelay = -1; @@ -328,7 +341,8 @@ export const getWateringData = async function( req: express.Request, res: expres * Makes an HTTP/HTTPS GET request to the specified URL and returns the response body. * @param url The URL to fetch. * @return A Promise that will be resolved the with response body if the request succeeds, or will be rejected with an - * Error if the request fails. + * error if the request fails or returns a non-200 status code. This error may contain information about the HTTP + * request or, response including API keys and other sensitive information. */ async function httpRequest( url: string ): Promise< string > { return new Promise< any >( ( resolve, reject ) => { @@ -343,6 +357,11 @@ async function httpRequest( url: string ): Promise< string > { }; ( isHttps ? https : http ).get( options, ( response ) => { + if ( response.statusCode !== 200 ) { + reject( `Received ${ response.statusCode } status code for URL '${ url }'.` ); + return; + } + let data = ""; // Reassemble the data as it comes in diff --git a/routes/weatherProviders/DarkSky.ts b/routes/weatherProviders/DarkSky.ts index 9655acf..8bbe377 100644 --- a/routes/weatherProviders/DarkSky.ts +++ b/routes/weatherProviders/DarkSky.ts @@ -17,12 +17,12 @@ async function getDarkSkyWateringData( coordinates: GeoCoordinates ): Promise< W yesterdayData = await httpJSONRequest( yesterdayUrl ); todayData = await httpJSONRequest( todayUrl ); } catch (err) { - // Indicate watering data could not be retrieved if an API error occurs. - return undefined; + console.error( "Error retrieving weather information from Dark Sky:", err ); + throw "An error occurred while retrieving weather information from Dark Sky." } if ( !todayData.hourly || !todayData.hourly.data || !yesterdayData.hourly || !yesterdayData.hourly.data ) { - return undefined; + throw "Necessary field(s) were missing from weather information returned by Dark Sky."; } /* The number of hourly forecasts to use from today's data. This will only include elements that contain historic @@ -39,7 +39,7 @@ async function getDarkSkyWateringData( coordinates: GeoCoordinates ): Promise< W // Fail if not enough data is available. if ( samples.length !== 24 ) { - return undefined; + throw "Insufficient data was returned by Dark Sky."; } const totals = { temp: 0, humidity: 0, precip: 0 }; @@ -66,12 +66,12 @@ async function getDarkSkyWeatherData( coordinates: GeoCoordinates ): Promise< We try { forecast = await httpJSONRequest( forecastUrl ); } catch (err) { - // Indicate weather data could not be retrieved if an API error occurs. - return undefined; + console.error( "Error retrieving weather information from Dark Sky:", err ); + throw "An error occurred while retrieving weather information from Dark Sky." } if ( !forecast.currently || !forecast.daily || !forecast.daily.data ) { - return undefined; + throw "Necessary field(s) were missing from weather information returned by Dark Sky."; } const weather: WeatherData = { diff --git a/routes/weatherProviders/OWM.ts b/routes/weatherProviders/OWM.ts index 096fae2..af86c48 100644 --- a/routes/weatherProviders/OWM.ts +++ b/routes/weatherProviders/OWM.ts @@ -10,13 +10,13 @@ async function getOWMWateringData( coordinates: GeoCoordinates ): Promise< Water try { forecast = await httpJSONRequest( forecastUrl ); } catch (err) { - // Indicate watering data could not be retrieved if an API error occurs. - return undefined; + console.error( "Error retrieving weather information from OWM:", err ); + throw "An error occurred while retrieving weather information from OWM." } // Indicate watering data could not be retrieved if the forecast data is incomplete. if ( !forecast || !forecast.list ) { - return undefined; + throw "Necessary field(s) were missing from weather information returned by OWM."; } let totalTemp = 0, @@ -49,13 +49,13 @@ async function getOWMWeatherData( coordinates: GeoCoordinates ): Promise< Weathe current = await httpJSONRequest( currentUrl ); forecast = await httpJSONRequest( forecastDailyUrl ); } catch (err) { - // Indicate watering data could not be retrieved if an API error occurs. - return undefined; + console.error( "Error retrieving weather information from OWM:", err ); + throw "An error occurred while retrieving weather information from OWM." } // Indicate watering data could not be retrieved if the forecast data is incomplete. if ( !current || !current.main || !current.wind || !current.weather || !forecast || !forecast.list ) { - return undefined; + throw "Necessary field(s) were missing from weather information returned by OWM."; } const weather: WeatherData = { diff --git a/types.ts b/types.ts index 6727935..8fcdf97 100644 --- a/types.ts +++ b/types.ts @@ -90,7 +90,7 @@ export interface WeatherProvider { * Retrieves weather data necessary for watering level calculations. * @param coordinates The coordinates to retrieve the watering data for. * @return A Promise that will be resolved with the WateringData if it is successfully retrieved, - * or resolved with undefined if an error occurs while retrieving the WateringData. + * or rejected with an error message if an error occurs while retrieving the WateringData. */ getWateringData?( coordinates : GeoCoordinates ): Promise< WateringData >; @@ -98,7 +98,7 @@ export interface WeatherProvider { * Retrieves the current weather data for usage in the mobile app. * @param coordinates The coordinates to retrieve the weather for * @return A Promise that will be resolved with the WeatherData if it is successfully retrieved, - * or resolved with undefined if an error occurs while retrieving the WeatherData. + * or rejected with an error message if an error occurs while retrieving the WeatherData. */ getWeatherData?( coordinates : GeoCoordinates ): Promise< WeatherData >; }