From 9b99b993ab48f1310cc82a7d02803d6ebe7a24f4 Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Sat, 24 Aug 2019 19:02:41 -0400 Subject: [PATCH 1/7] Add error codes to watering data errors --- errors.ts | 80 +++++++++++++++++++ routes/adjustmentMethods/AdjustmentMethod.ts | 8 +- .../adjustmentMethods/EToAdjustmentMethod.ts | 7 +- .../ZimmermanAdjustmentMethod.ts | 4 +- routes/weather.ts | 75 ++++++++++------- routes/weatherProviders/DarkSky.ts | 9 ++- routes/weatherProviders/OWM.ts | 9 ++- routes/weatherProviders/WUnderground.ts | 7 +- routes/weatherProviders/WeatherProvider.ts | 14 ++-- tsconfig.json | 1 + 10 files changed, 159 insertions(+), 55 deletions(-) create mode 100644 errors.ts diff --git a/errors.ts b/errors.ts new file mode 100644 index 0000000..e9cfe6d --- /dev/null +++ b/errors.ts @@ -0,0 +1,80 @@ +export enum ErrorCode { + /** An error was not properly handled and assigned a more specific error code. */ + UnexpectedError = 0, + + /** The watering scale could not be calculated due to a problem with the weather information. */ + BadWeatherData = 1, + /** Data for a full 24 hour period was not available. */ + InsufficientWeatherData = 10, + /** A necessary field was missing from weather data returned by the API. */ + MissingWeatherField = 11, + /** An HTTP or parsing error occurred when retrieving weather information. */ + WeatherApiError = 12, + + /** The specified location name could not be resolved. */ + LocationError = 2, + /** An HTTP or parsing error occurred when resolving the location. */ + LocationServiceApiError = 20, + /** No matches were found for the specified location name. */ + NoLocationFound = 21, + /** The location name was specified in an invalid format (e.g. a PWS ID). */ + InvalidLocationFormat = 22, + + /** An Error related to personal weather stations. */ + PwsError = 3, + /** The PWS ID did not use the correct format. */ + InvalidPwsId = 30, + /** The PWS API key did not use the correct format. */ + InvalidPwsApiKey = 31, + // TODO use this error code. + /** The PWS API returned an error because a bad API key was specified. */ + PwsAuthenticationError = 32, + /** A PWS was specified but the data for the specified AdjustmentMethod cannot be retrieved from a PWS. */ + PwsNotSupported = 33, + /** A PWS is required by the WeatherProvider but was not provided. */ + NoPwsProvided = 34, + + /** An error related to AdjustmentMethods or watering restrictions. */ + AdjustmentMethodError = 4, + /** The WeatherProvider is incompatible with the specified AdjustmentMethod. */ + UnsupportedAdjustmentMethod = 40, + /** An invalid AdjustmentMethod ID was specified. */ + InvalidAdjustmentMethod = 41, + + /** An error related to adjustment options (wto). */ + AdjustmentOptionsError = 5, + /** The adjustment options could not be parsed. */ + MalformedAdjustmentOptions = 50, + /** A required adjustment option was not provided. */ + MissingAdjustmentOption = 51 +} + +/** An error with a numeric code that can be used to identify the type of error. */ +export class CodedError { + public readonly errCode: ErrorCode; + public readonly message: string; + + public constructor( errCode: ErrorCode, message: string ) { + this.errCode = errCode; + this.message = message; + } +} + +/** + * Returns a CodedError representing the specified error. This function can be used to ensure that errors caught in try-catch + * statements have an error code and do not contain any sensitive information in the error message. If `err` is a + * CodedError, the same object will be returned. If `err` is not a CodedError, it is assumed that the error wasn't + * properly handled, so a CodedError with a generic message and an "UnexpectedError" code will be returned. This ensures + * that the user will only be sent errors that were initially raised by the OpenSprinkler weather service and have + * had any sensitive information (like API keys) removed from the error message. + * @param err Any error caught in a try-catch statement. + * @return A CodedError representing the error that was passed to the function. + */ +export function makeCodedError( err: any ): CodedError { + if ( err instanceof CodedError ) { + return err; + } else { + // Include the current timestamp in the error message so the full error can easily be found in the logs. + return new CodedError( ErrorCode.UnexpectedError, "Unexpected error occurred at timestamp " + Date.now() ); + } +} diff --git a/routes/adjustmentMethods/AdjustmentMethod.ts b/routes/adjustmentMethods/AdjustmentMethod.ts index 9b37574..2a6aaba 100644 --- a/routes/adjustmentMethods/AdjustmentMethod.ts +++ b/routes/adjustmentMethods/AdjustmentMethod.ts @@ -1,5 +1,6 @@ import { BaseWateringData, GeoCoordinates, PWS } from "../../types"; import { WeatherProvider } from "../weatherProviders/WeatherProvider"; +import { ErrorCode } from "../../errors"; export interface AdjustmentMethod { @@ -14,7 +15,7 @@ export interface AdjustmentMethod { * of this method does not have PWS support, this parameter may be ignored and coordinates may be used instead. * @return A Promise that will be resolved with the result of the calculation, or rejected with an error message if * the watering scale cannot be calculated. - * @throws An error message can be thrown if an error occurs while calculating the watering scale. + * @throws A CodedError may be thrown if an error occurs while calculating the watering scale. */ calculateWateringScale( adjustmentOptions: AdjustmentOptions, @@ -43,7 +44,6 @@ export interface AdjustmentMethodResponse { * watering. */ rainDelay?: number; - // TODO consider removing this field and breaking backwards compatibility to handle all errors consistently. /** * An message to send to the OS firmware to indicate that an error occurred while calculating the watering * scale and the returned scale either defaulted to some reasonable value or was calculated with incomplete data. @@ -51,7 +51,9 @@ export interface AdjustmentMethodResponse { * but newer firmware versions may be able to alert the user that an error occurred and/or default to a * user-configured watering scale instead of using the one returned by the AdjustmentMethod. */ - errorMessage?: string; + errMessage?: string; + /** A code describing the type of error that occurred (if one occurred). */ + errCode?: ErrorCode; /** The data that was used to calculate the watering scale, or undefined if no data was used. */ wateringData: BaseWateringData; } diff --git a/routes/adjustmentMethods/EToAdjustmentMethod.ts b/routes/adjustmentMethods/EToAdjustmentMethod.ts index 03a7846..29afb96 100644 --- a/routes/adjustmentMethods/EToAdjustmentMethod.ts +++ b/routes/adjustmentMethods/EToAdjustmentMethod.ts @@ -3,6 +3,7 @@ import * as moment from "moment"; import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from "./AdjustmentMethod"; import { BaseWateringData, GeoCoordinates, PWS } from "../../types"; import { WeatherProvider } from "../weatherProviders/WeatherProvider"; +import { CodedError, ErrorCode } from "../../errors"; /** @@ -17,7 +18,7 @@ async function calculateEToWateringScale( ): Promise< AdjustmentMethodResponse > { if ( pws ) { - throw "ETo adjustment method does not support personal weather stations through WUnderground."; + throw new CodedError( ErrorCode.PwsNotSupported, "ETo adjustment method does not support personal weather stations through WUnderground." ); } // Temporarily disabled since OWM forecast data is checking if rain is forecasted for 3 hours in the future. @@ -30,7 +31,7 @@ async function calculateEToWateringScale( } */ - // This will throw an error message if ETo data cannot be retrieved. + // This will throw a CodedError if ETo data cannot be retrieved. const etoData: EToData = await weatherProvider.getEToData( coordinates ); let baseETo: number; @@ -40,7 +41,7 @@ async function calculateEToWateringScale( if ( adjustmentOptions && "baseETo" in adjustmentOptions ) { baseETo = adjustmentOptions.baseETo } else { - throw "A baseline potential ETo must be provided."; + throw new CodedError( ErrorCode.MissingAdjustmentOption, "A baseline potential ETo must be provided." ); } if ( adjustmentOptions && "elevation" in adjustmentOptions ) { diff --git a/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts b/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts index e8fea63..d4ba15b 100644 --- a/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts +++ b/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts @@ -2,6 +2,7 @@ import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from ". import { GeoCoordinates, PWS, ZimmermanWateringData } from "../../types"; import { validateValues } from "../weather"; import { WeatherProvider } from "../weatherProviders/WeatherProvider"; +import { ErrorCode } from "../../errors"; /** @@ -41,7 +42,8 @@ async function calculateZimmermanWateringScale( return { scale: 100, rawData: rawData, - errorMessage: "Necessary field(s) were missing from ZimmermanWateringData.", + errCode: ErrorCode.MissingWeatherField, + errMessage: "Necessary field(s) were missing from ZimmermanWateringData.", wateringData: wateringData }; } diff --git a/routes/weather.ts b/routes/weather.ts index fb2ffc7..1fca233 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -13,6 +13,8 @@ import ManualAdjustmentMethod from "./adjustmentMethods/ManualAdjustmentMethod"; import ZimmermanAdjustmentMethod from "./adjustmentMethods/ZimmermanAdjustmentMethod"; import RainDelayAdjustmentMethod from "./adjustmentMethods/RainDelayAdjustmentMethod"; import EToAdjustmentMethod from "./adjustmentMethods/EToAdjustmentMethod"; +import { CodedError, ErrorCode, makeCodedError } from "../errors"; + const WEATHER_PROVIDER: WeatherProvider = new ( require("./weatherProviders/" + ( process.env.WEATHER_PROVIDER || "OWM" ) ).default )(); const PWS_WEATHER_PROVIDER: WeatherProvider = new ( require("./weatherProviders/" + ( process.env.PWS_WEATHER_PROVIDER || "WUnderground" ) ).default )(); @@ -39,16 +41,16 @@ const cache = new WateringScaleCache(); * Resolves a location description to geographic coordinates. * @param location A partial zip/city/country or a coordinate pair. * @return A promise that will be resolved with the coordinates of the best match for the specified location, or - * rejected with an error message if unable to resolve the location. + * rejected with a CodedError if unable to resolve the location. */ export async function resolveCoordinates( location: string ): Promise< GeoCoordinates > { if ( !location ) { - throw "No location specified"; + throw new CodedError( ErrorCode.InvalidLocationFormat, "No location specified" ); } if ( filters.pws.test( location ) ) { - throw "PWS ID must be specified in the pws parameter."; + throw new CodedError( ErrorCode.InvalidLocationFormat, "PWS ID must be specified in the pws adjustment option." ); } else if ( filters.gps.test( location ) ) { const split: string[] = location.split( "," ); return [ parseFloat( split[ 0 ] ), parseFloat( split[ 1 ] ) ]; @@ -62,7 +64,7 @@ export async function resolveCoordinates( location: string ): Promise< GeoCoordi data = await httpJSONRequest( url ); } catch (err) { // If the request fails, indicate no data was found. - throw "An API error occurred while attempting to resolve location"; + throw new CodedError( ErrorCode.LocationServiceApiError, "An API error occurred while attempting to resolve location" ); } // Check if the data is valid @@ -73,7 +75,7 @@ export async function resolveCoordinates( location: string ): Promise< GeoCoordi } else { // Otherwise, indicate no data was found - throw "No match found for specified location"; + throw new CodedError( ErrorCode.NoLocationFound, "No match found for specified location" ); } } } @@ -194,7 +196,7 @@ export const getWateringData = async function( req: express.Request, res: expres remoteAddress = remoteAddress.split( "," )[ 0 ]; if ( !adjustmentMethod ) { - res.send( "Error: Unknown AdjustmentMethod ID" ); + sendWateringData( res, { errCode: ErrorCode.InvalidAdjustmentMethod, errMessage: "Invalid AdjustmentMethod ID" } ); return; } @@ -207,9 +209,8 @@ export const getWateringData = async function( req: express.Request, res: expres // Reconstruct JSON string from deformed controller output adjustmentOptions = JSON.parse( "{" + adjustmentOptionsString + "}" ); } catch ( err ) { - - // If the JSON is not valid then abort the claculation - res.send(`Error: Unable to parse options (${err})`); + // If the JSON is not valid then abort the calculation + sendWateringData( res, { errCode: ErrorCode.MalformedAdjustmentOptions, errMessage: `Unable to parse options (${ err })` } ); return; } @@ -217,8 +218,13 @@ export const getWateringData = async function( req: express.Request, res: expres let coordinates: GeoCoordinates; try { coordinates = await resolveCoordinates( location ); - } catch (err) { - res.send(`Error: Unable to resolve location (${err})`); + } catch ( err ) { + let codedError: CodedError = makeCodedError( err ); + if ( codedError.errCode === ErrorCode.UnexpectedError ) { + console.error( `An unexpected error occurred during location resolution for "${ req.url }": `, err ); + } + + sendWateringData( res, { errCode: codedError.errCode, errMessage: `Unable to resolve location "${ location }" (${ codedError.message })` } ); return; } @@ -235,11 +241,11 @@ export const getWateringData = async function( req: express.Request, res: expres // Make sure that the PWS ID and API key look valid. if ( !pwsId ) { - res.send("Error: PWS ID does not appear to be valid."); + sendWateringData( res, { errCode: ErrorCode.InvalidPwsId, errMessage: "PWS ID does not appear to be valid" } ); return; } if ( !apiKey ) { - res.send("Error: PWS API key does not appear to be valid."); + sendWateringData( res, { errCode: ErrorCode.InvalidPwsApiKey, errMessage: "PWS API key does not appear to be valid" } ); return; } @@ -256,7 +262,7 @@ export const getWateringData = async function( req: express.Request, res: expres sunset: timeData.sunset, eip: ipToInt( remoteAddress ), rawData: undefined, - error: undefined + errMessage: undefined }; let cachedScale: CachedScale; @@ -277,22 +283,17 @@ export const getWateringData = async function( req: express.Request, res: expres adjustmentOptions, coordinates, weatherProvider, pws ); } catch ( err ) { - if ( typeof err != "string" ) { - /* If an error occurs under expected circumstances (e.g. required optional fields from a weather API are - missing), an AdjustmentOption must throw a string. If a non-string error is caught, it is likely an Error - thrown by the JS engine due to unexpected circumstances. The user should not be shown the error message - since it may contain sensitive information. */ - res.send( "Error: an unexpected error occurred." ); - console.error( `An unexpected error occurred for ${ req.url }: `, err ); - } else { - res.send( "Error: " + err ); + let codedError: CodedError = makeCodedError( err ); + if ( codedError.errCode === ErrorCode.UnexpectedError ) { + console.error( `An unexpected error occurred during watering scale calculation for "${ req.url }": `, err ); } + sendWateringData( res, { errCode: codedError.errCode, errMessage: codedError.message } ); return; } data.scale = adjustmentMethodResponse.scale; - data.error = adjustmentMethodResponse.errorMessage; + data.errMessage = adjustmentMethodResponse.errMessage; data.rd = adjustmentMethodResponse.rainDelay; data.rawData = adjustmentMethodResponse.rawData; @@ -303,7 +304,12 @@ export const getWateringData = async function( req: express.Request, res: expres try { wateringData = await weatherProvider.getWateringData( coordinates ); } catch ( err ) { - res.send( "Error: " + err ); + let codedError: CodedError = makeCodedError( err ); + if ( codedError.errCode === ErrorCode.UnexpectedError ) { + console.error( `An unexpected error occurred during restriction checks for "${ req.url }": `, err ); + } + + sendWateringData( res, { errCode: codedError.errCode, errMessage: codedError.message } ); return; } } @@ -315,7 +321,7 @@ export const getWateringData = async function( req: express.Request, res: expres } // Cache the watering scale if caching is enabled and no error occurred. - if ( weatherProvider.shouldCacheWateringScale() && !data.error ) { + if ( weatherProvider.shouldCacheWateringScale() && !data.errMessage ) { cache.storeWateringScale( req.params[ 0 ], coordinates, pws, adjustmentOptions, { scale: data.scale, rawData: data.rawData, @@ -324,8 +330,17 @@ export const getWateringData = async function( req: express.Request, res: expres } } - // Return the response to the client in the requested format - if ( outputFormat === "json" ) { + sendWateringData( res, data, outputFormat === "json" ); +}; + +/** + * Sends a response to an HTTP request with a 200 status code. + * @param res The Express Response object to send the response through. + * @param data An object containing key/value pairs that should be formatted in the response body. + * @param useJson Indicates if the response body should use a JSON format instead of a format similar to URL query strings. + */ +function sendWateringData( res: express.Response, data: object, useJson: boolean = false ) { + if ( useJson ) { res.json( data ); } else { // Return the data formatted as a URL query string. @@ -344,7 +359,7 @@ export const getWateringData = async function( req: express.Request, res: expres case "object": // Convert objects to JSON. value = JSON.stringify( value ); - // Fallthrough. + // Fallthrough. case "string": /* URL encode strings. Since the OS firmware uses a primitive version of query string parsing and decoding, only some characters need to be escaped and only spaces ("+" or "%20") will be decoded. */ @@ -356,7 +371,7 @@ export const getWateringData = async function( req: express.Request, res: expres } res.send( formatted ); } -}; +} /** * Makes an HTTP/HTTPS GET request to the specified URL and returns the response body. diff --git a/routes/weatherProviders/DarkSky.ts b/routes/weatherProviders/DarkSky.ts index bb24837..5c6f336 100644 --- a/routes/weatherProviders/DarkSky.ts +++ b/routes/weatherProviders/DarkSky.ts @@ -4,6 +4,7 @@ import { GeoCoordinates, WeatherData, ZimmermanWateringData } from "../../types" import { httpJSONRequest } from "../weather"; import { WeatherProvider } from "./WeatherProvider"; import { approximateSolarRadiation, CloudCoverInfo, EToData } from "../adjustmentMethods/EToAdjustmentMethod"; +import { CodedError, ErrorCode } from "../../errors"; export default class DarkSkyWeatherProvider extends WeatherProvider { @@ -28,11 +29,11 @@ export default class DarkSkyWeatherProvider extends WeatherProvider { yesterdayData = await httpJSONRequest( yesterdayUrl ); } catch ( err ) { console.error( "Error retrieving weather information from Dark Sky:", err ); - throw "An error occurred while retrieving weather information from Dark Sky." + throw new CodedError( ErrorCode.WeatherApiError, "An error occurred while retrieving weather information from Dark Sky." ); } if ( !yesterdayData.hourly || !yesterdayData.hourly.data ) { - throw "Necessary field(s) were missing from weather information returned by Dark Sky."; + throw new CodedError( ErrorCode.MissingWeatherField, "Necessary field(s) were missing from weather information returned by Dark Sky." ); } const samples = [ @@ -41,7 +42,7 @@ export default class DarkSkyWeatherProvider extends WeatherProvider { // Fail if not enough data is available. if ( samples.length !== 24 ) { - throw "Insufficient data was returned by Dark Sky."; + throw new CodedError( ErrorCode.InsufficientWeatherData, "Insufficient data was returned by Dark Sky." ); } const totals = { temp: 0, humidity: 0, precip: 0 }; @@ -122,7 +123,7 @@ export default class DarkSkyWeatherProvider extends WeatherProvider { try { historicData = await httpJSONRequest( historicUrl ); } catch (err) { - throw "An error occurred while retrieving weather information from Dark Sky." + throw new CodedError( ErrorCode.WeatherApiError, "An error occurred while retrieving weather information from Dark Sky." ); } const cloudCoverInfo: CloudCoverInfo[] = historicData.hourly.data.map( ( hour ): CloudCoverInfo => { diff --git a/routes/weatherProviders/OWM.ts b/routes/weatherProviders/OWM.ts index cf109c6..493929a 100644 --- a/routes/weatherProviders/OWM.ts +++ b/routes/weatherProviders/OWM.ts @@ -3,6 +3,7 @@ import { httpJSONRequest } from "../weather"; import { WeatherProvider } from "./WeatherProvider"; import { approximateSolarRadiation, CloudCoverInfo, EToData } from "../adjustmentMethods/EToAdjustmentMethod"; import * as moment from "moment"; +import { CodedError, ErrorCode } from "../../errors"; export default class OWMWeatherProvider extends WeatherProvider { @@ -25,12 +26,12 @@ export default class OWMWeatherProvider extends WeatherProvider { forecast = await httpJSONRequest( forecastUrl ); } catch ( err ) { console.error( "Error retrieving weather information from OWM:", err ); - throw "An error occurred while retrieving weather information from OWM." + throw new CodedError( ErrorCode.WeatherApiError, "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 ) { - throw "Necessary field(s) were missing from weather information returned by OWM."; + throw new CodedError( ErrorCode.MissingWeatherField, "Necessary field(s) were missing from weather information returned by OWM." ); } let totalTemp = 0, @@ -111,12 +112,12 @@ export default class OWMWeatherProvider extends WeatherProvider { forecast = await httpJSONRequest( forecastUrl ); } catch (err) { console.error( "Error retrieving ETo information from OWM:", err ); - throw "An error occurred while retrieving ETo information from OWM." + throw new CodedError( ErrorCode.WeatherApiError, "An error occurred while retrieving ETo information from OWM." ); } // Indicate ETo data could not be retrieved if the forecast data is incomplete. if ( !forecast || !forecast.list || forecast.list.length < 8 ) { - throw "Insufficient data available from OWM." + throw new CodedError( ErrorCode.InsufficientWeatherData, "Insufficient data available from OWM." ); } // Take a sample over 24 hours. diff --git a/routes/weatherProviders/WUnderground.ts b/routes/weatherProviders/WUnderground.ts index 854af36..71114be 100644 --- a/routes/weatherProviders/WUnderground.ts +++ b/routes/weatherProviders/WUnderground.ts @@ -1,12 +1,13 @@ import { GeoCoordinates, PWS, WeatherData, ZimmermanWateringData } from "../../types"; import { WeatherProvider } from "./WeatherProvider"; import { httpJSONRequest } from "../weather"; +import { CodedError, ErrorCode } from "../../errors"; export default class WUnderground extends WeatherProvider { async getWateringData( coordinates: GeoCoordinates, pws?: PWS ): Promise< ZimmermanWateringData > { if ( !pws ) { - throw "WUnderground WeatherProvider requires a PWS to be specified."; + throw new CodedError( ErrorCode.NoPwsProvided, "WUnderground WeatherProvider requires a PWS to be specified." ); } const url = `https://api.weather.com/v2/pws/observations/hourly/7day?stationId=${ pws.id }&format=json&units=e&apiKey=${ pws.apiKey }`; @@ -15,7 +16,7 @@ export default class WUnderground extends WeatherProvider { data = await httpJSONRequest( url ); } catch ( err ) { console.error( "Error retrieving weather information from WUnderground:", err ); - throw "An error occurred while retrieving weather information from WUnderground." + throw new CodedError( ErrorCode.WeatherApiError, "An error occurred while retrieving weather information from WUnderground." ); } // Take the 24 most recent observations. @@ -23,7 +24,7 @@ export default class WUnderground extends WeatherProvider { // Fail if not enough data is available. if ( samples.length !== 24 ) { - throw "Insufficient data was returned by WUnderground."; + throw new CodedError( ErrorCode.InsufficientWeatherData, "Insufficient data was returned by WUnderground." ); } const totals = { temp: 0, humidity: 0, precip: 0 }; diff --git a/routes/weatherProviders/WeatherProvider.ts b/routes/weatherProviders/WeatherProvider.ts index e837d6a..0074fbe 100644 --- a/routes/weatherProviders/WeatherProvider.ts +++ b/routes/weatherProviders/WeatherProvider.ts @@ -1,5 +1,6 @@ import { GeoCoordinates, PWS, WeatherData, ZimmermanWateringData } from "../../types"; import { EToData } from "../adjustmentMethods/EToAdjustmentMethod"; +import { CodedError, ErrorCode } from "../../errors"; export class WeatherProvider { /** @@ -8,11 +9,11 @@ export class WeatherProvider { * @param pws The PWS to retrieve the weather from, or undefined if a PWS should not be used. If the implementation * of this method does not have PWS support, this parameter may be ignored and coordinates may be used instead. * @return A Promise that will be resolved with the ZimmermanWateringData if it is successfully retrieved, - * or rejected with an error message if an error occurs while retrieving the ZimmermanWateringData or the WeatherProvider - * does not support this method. + * or rejected with a CodedError if an error occurs while retrieving the ZimmermanWateringData (or the WeatherProvider + * does not support this method). */ getWateringData( coordinates: GeoCoordinates, pws?: PWS ): Promise< ZimmermanWateringData > { - throw "Selected WeatherProvider does not support getWateringData"; + throw new CodedError( ErrorCode.UnsupportedAdjustmentMethod, "Selected WeatherProvider does not support getWateringData" ); } /** @@ -29,12 +30,11 @@ export class WeatherProvider { /** * Retrieves the data necessary for calculating potential ETo. * @param coordinates The coordinates to retrieve the data for. - * @return A Promise that will be resolved with the EToData if it is successfully retrieved, - * or rejected with an error message if an error occurs while retrieving the EToData or the WeatherProvider does - * not support this method. + * @return A Promise that will be resolved with the EToData if it is successfully retrieved, or rejected with a + * CodedError if an error occurs while retrieving the EToData (or the WeatherProvider does not support this method). */ getEToData( coordinates: GeoCoordinates ): Promise< EToData > { - throw "Selected WeatherProvider does not support getEToData"; + throw new CodedError( ErrorCode.UnsupportedAdjustmentMethod, "Selected WeatherProvider does not support getEToData" ); }; /** diff --git a/tsconfig.json b/tsconfig.json index aaf0e43..ed9ea4d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "skipLibCheck": true }, "include": [ + "errors.ts", "server.ts", "types.ts", "routes/**/*" From d77553bce4a5b4d91b901c881e7db7165cd50d7d Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Sat, 31 Aug 2019 18:36:25 -0400 Subject: [PATCH 2/7] Include an error code in every response --- errors.ts | 11 ++++++++--- routes/weather.ts | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/errors.ts b/errors.ts index e9cfe6d..c6e6c25 100644 --- a/errors.ts +++ b/errors.ts @@ -1,6 +1,8 @@ export enum ErrorCode { - /** An error was not properly handled and assigned a more specific error code. */ - UnexpectedError = 0, + /** No error occurred. This code should be included with all successful responses because the firmware expects some + * code to be present. + */ + NoError = 0, /** The watering scale could not be calculated due to a problem with the weather information. */ BadWeatherData = 1, @@ -46,7 +48,10 @@ export enum ErrorCode { /** The adjustment options could not be parsed. */ MalformedAdjustmentOptions = 50, /** A required adjustment option was not provided. */ - MissingAdjustmentOption = 51 + MissingAdjustmentOption = 51, + + /** An error was not properly handled and assigned a more specific error code. */ + UnexpectedError = 99 } /** An error with a numeric code that can be used to identify the type of error. */ diff --git a/routes/weather.ts b/routes/weather.ts index 1fca233..e3411d6 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -262,7 +262,8 @@ export const getWateringData = async function( req: express.Request, res: expres sunset: timeData.sunset, eip: ipToInt( remoteAddress ), rawData: undefined, - errMessage: undefined + errMessage: undefined, + errCode: 0 }; let cachedScale: CachedScale; @@ -293,6 +294,7 @@ export const getWateringData = async function( req: express.Request, res: expres } data.scale = adjustmentMethodResponse.scale; + data.errCode = adjustmentMethodResponse.errCode || 0; data.errMessage = adjustmentMethodResponse.errMessage; data.rd = adjustmentMethodResponse.rainDelay; data.rawData = adjustmentMethodResponse.rawData; From f8d3c64a339c93d7acc04b2f566edfeeae6e0f86 Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Sun, 1 Sep 2019 11:23:56 -0400 Subject: [PATCH 3/7] Fix tests --- test/expected.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/expected.json b/test/expected.json index fdcdf8f..16b6136 100644 --- a/test/expected.json +++ b/test/expected.json @@ -4,7 +4,8 @@ "tz": 32, "sunrise": 332, "sunset": 1203, - "eip": 2130706433 + "eip": 2130706433, + "errCode": 0 } }, "adjustment1": { @@ -19,7 +20,8 @@ "p": 1.09, "t": 70.8, "raining": 1 - } + }, + "errCode": 0 } } } From 2918527bcba6b6b25b226ab4b2284e82147a2c83 Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Sun, 1 Sep 2019 15:05:41 -0400 Subject: [PATCH 4/7] Remove errMessage --- errors.ts | 6 ++--- routes/adjustmentMethods/AdjustmentMethod.ts | 8 ------ .../adjustmentMethods/EToAdjustmentMethod.ts | 4 +-- .../ZimmermanAdjustmentMethod.ts | 1 - routes/weather.ts | 26 +++++++++---------- routes/weatherProviders/DarkSky.ts | 8 +++--- routes/weatherProviders/OWM.ts | 8 +++--- routes/weatherProviders/WUnderground.ts | 6 ++--- routes/weatherProviders/WeatherProvider.ts | 4 +-- 9 files changed, 29 insertions(+), 42 deletions(-) diff --git a/errors.ts b/errors.ts index c6e6c25..1974697 100644 --- a/errors.ts +++ b/errors.ts @@ -59,9 +59,8 @@ export class CodedError { public readonly errCode: ErrorCode; public readonly message: string; - public constructor( errCode: ErrorCode, message: string ) { + public constructor( errCode: ErrorCode ) { this.errCode = errCode; - this.message = message; } } @@ -79,7 +78,6 @@ export function makeCodedError( err: any ): CodedError { if ( err instanceof CodedError ) { return err; } else { - // Include the current timestamp in the error message so the full error can easily be found in the logs. - return new CodedError( ErrorCode.UnexpectedError, "Unexpected error occurred at timestamp " + Date.now() ); + return new CodedError( ErrorCode.UnexpectedError ); } } diff --git a/routes/adjustmentMethods/AdjustmentMethod.ts b/routes/adjustmentMethods/AdjustmentMethod.ts index 2a6aaba..f80a3b1 100644 --- a/routes/adjustmentMethods/AdjustmentMethod.ts +++ b/routes/adjustmentMethods/AdjustmentMethod.ts @@ -44,14 +44,6 @@ export interface AdjustmentMethodResponse { * watering. */ rainDelay?: number; - /** - * An message to send to the OS firmware to indicate that an error occurred while calculating the watering - * scale and the returned scale either defaulted to some reasonable value or was calculated with incomplete data. - * Older firmware versions will ignore this field (they will silently swallow the error and use the returned scale), - * but newer firmware versions may be able to alert the user that an error occurred and/or default to a - * user-configured watering scale instead of using the one returned by the AdjustmentMethod. - */ - errMessage?: string; /** A code describing the type of error that occurred (if one occurred). */ errCode?: ErrorCode; /** The data that was used to calculate the watering scale, or undefined if no data was used. */ diff --git a/routes/adjustmentMethods/EToAdjustmentMethod.ts b/routes/adjustmentMethods/EToAdjustmentMethod.ts index 29afb96..abfba8a 100644 --- a/routes/adjustmentMethods/EToAdjustmentMethod.ts +++ b/routes/adjustmentMethods/EToAdjustmentMethod.ts @@ -18,7 +18,7 @@ async function calculateEToWateringScale( ): Promise< AdjustmentMethodResponse > { if ( pws ) { - throw new CodedError( ErrorCode.PwsNotSupported, "ETo adjustment method does not support personal weather stations through WUnderground." ); + throw new CodedError( ErrorCode.PwsNotSupported ); } // Temporarily disabled since OWM forecast data is checking if rain is forecasted for 3 hours in the future. @@ -41,7 +41,7 @@ async function calculateEToWateringScale( if ( adjustmentOptions && "baseETo" in adjustmentOptions ) { baseETo = adjustmentOptions.baseETo } else { - throw new CodedError( ErrorCode.MissingAdjustmentOption, "A baseline potential ETo must be provided." ); + throw new CodedError( ErrorCode.MissingAdjustmentOption ); } if ( adjustmentOptions && "elevation" in adjustmentOptions ) { diff --git a/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts b/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts index d4ba15b..bbbc7e3 100644 --- a/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts +++ b/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts @@ -43,7 +43,6 @@ async function calculateZimmermanWateringScale( scale: 100, rawData: rawData, errCode: ErrorCode.MissingWeatherField, - errMessage: "Necessary field(s) were missing from ZimmermanWateringData.", wateringData: wateringData }; } diff --git a/routes/weather.ts b/routes/weather.ts index e3411d6..047f88d 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -46,11 +46,11 @@ const cache = new WateringScaleCache(); export async function resolveCoordinates( location: string ): Promise< GeoCoordinates > { if ( !location ) { - throw new CodedError( ErrorCode.InvalidLocationFormat, "No location specified" ); + throw new CodedError( ErrorCode.InvalidLocationFormat ); } if ( filters.pws.test( location ) ) { - throw new CodedError( ErrorCode.InvalidLocationFormat, "PWS ID must be specified in the pws adjustment option." ); + throw new CodedError( ErrorCode.InvalidLocationFormat ); } else if ( filters.gps.test( location ) ) { const split: string[] = location.split( "," ); return [ parseFloat( split[ 0 ] ), parseFloat( split[ 1 ] ) ]; @@ -64,7 +64,7 @@ export async function resolveCoordinates( location: string ): Promise< GeoCoordi data = await httpJSONRequest( url ); } catch (err) { // If the request fails, indicate no data was found. - throw new CodedError( ErrorCode.LocationServiceApiError, "An API error occurred while attempting to resolve location" ); + throw new CodedError( ErrorCode.LocationServiceApiError ); } // Check if the data is valid @@ -75,7 +75,7 @@ export async function resolveCoordinates( location: string ): Promise< GeoCoordi } else { // Otherwise, indicate no data was found - throw new CodedError( ErrorCode.NoLocationFound, "No match found for specified location" ); + throw new CodedError( ErrorCode.NoLocationFound ); } } } @@ -196,7 +196,7 @@ export const getWateringData = async function( req: express.Request, res: expres remoteAddress = remoteAddress.split( "," )[ 0 ]; if ( !adjustmentMethod ) { - sendWateringData( res, { errCode: ErrorCode.InvalidAdjustmentMethod, errMessage: "Invalid AdjustmentMethod ID" } ); + sendWateringData( res, { errCode: ErrorCode.InvalidAdjustmentMethod } ); return; } @@ -210,7 +210,7 @@ export const getWateringData = async function( req: express.Request, res: expres adjustmentOptions = JSON.parse( "{" + adjustmentOptionsString + "}" ); } catch ( err ) { // If the JSON is not valid then abort the calculation - sendWateringData( res, { errCode: ErrorCode.MalformedAdjustmentOptions, errMessage: `Unable to parse options (${ err })` } ); + sendWateringData( res, { errCode: ErrorCode.MalformedAdjustmentOptions } ); return; } @@ -224,7 +224,7 @@ export const getWateringData = async function( req: express.Request, res: expres console.error( `An unexpected error occurred during location resolution for "${ req.url }": `, err ); } - sendWateringData( res, { errCode: codedError.errCode, errMessage: `Unable to resolve location "${ location }" (${ codedError.message })` } ); + sendWateringData( res, { errCode: codedError.errCode } ); return; } @@ -241,11 +241,11 @@ export const getWateringData = async function( req: express.Request, res: expres // Make sure that the PWS ID and API key look valid. if ( !pwsId ) { - sendWateringData( res, { errCode: ErrorCode.InvalidPwsId, errMessage: "PWS ID does not appear to be valid" } ); + sendWateringData( res, { errCode: ErrorCode.InvalidPwsId } ); return; } if ( !apiKey ) { - sendWateringData( res, { errCode: ErrorCode.InvalidPwsApiKey, errMessage: "PWS API key does not appear to be valid" } ); + sendWateringData( res, { errCode: ErrorCode.InvalidPwsApiKey } ); return; } @@ -262,7 +262,6 @@ export const getWateringData = async function( req: express.Request, res: expres sunset: timeData.sunset, eip: ipToInt( remoteAddress ), rawData: undefined, - errMessage: undefined, errCode: 0 }; @@ -289,13 +288,12 @@ export const getWateringData = async function( req: express.Request, res: expres console.error( `An unexpected error occurred during watering scale calculation for "${ req.url }": `, err ); } - sendWateringData( res, { errCode: codedError.errCode, errMessage: codedError.message } ); + sendWateringData( res, { errCode: codedError.errCode } ); return; } data.scale = adjustmentMethodResponse.scale; data.errCode = adjustmentMethodResponse.errCode || 0; - data.errMessage = adjustmentMethodResponse.errMessage; data.rd = adjustmentMethodResponse.rainDelay; data.rawData = adjustmentMethodResponse.rawData; @@ -311,7 +309,7 @@ export const getWateringData = async function( req: express.Request, res: expres console.error( `An unexpected error occurred during restriction checks for "${ req.url }": `, err ); } - sendWateringData( res, { errCode: codedError.errCode, errMessage: codedError.message } ); + sendWateringData( res, { errCode: codedError.errCode } ); return; } } @@ -323,7 +321,7 @@ export const getWateringData = async function( req: express.Request, res: expres } // Cache the watering scale if caching is enabled and no error occurred. - if ( weatherProvider.shouldCacheWateringScale() && !data.errMessage ) { + if ( weatherProvider.shouldCacheWateringScale() ) { cache.storeWateringScale( req.params[ 0 ], coordinates, pws, adjustmentOptions, { scale: data.scale, rawData: data.rawData, diff --git a/routes/weatherProviders/DarkSky.ts b/routes/weatherProviders/DarkSky.ts index 5c6f336..f8133b7 100644 --- a/routes/weatherProviders/DarkSky.ts +++ b/routes/weatherProviders/DarkSky.ts @@ -29,11 +29,11 @@ export default class DarkSkyWeatherProvider extends WeatherProvider { yesterdayData = await httpJSONRequest( yesterdayUrl ); } catch ( err ) { console.error( "Error retrieving weather information from Dark Sky:", err ); - throw new CodedError( ErrorCode.WeatherApiError, "An error occurred while retrieving weather information from Dark Sky." ); + throw new CodedError( ErrorCode.WeatherApiError ); } if ( !yesterdayData.hourly || !yesterdayData.hourly.data ) { - throw new CodedError( ErrorCode.MissingWeatherField, "Necessary field(s) were missing from weather information returned by Dark Sky." ); + throw new CodedError( ErrorCode.MissingWeatherField ); } const samples = [ @@ -42,7 +42,7 @@ export default class DarkSkyWeatherProvider extends WeatherProvider { // Fail if not enough data is available. if ( samples.length !== 24 ) { - throw new CodedError( ErrorCode.InsufficientWeatherData, "Insufficient data was returned by Dark Sky." ); + throw new CodedError( ErrorCode.InsufficientWeatherData ); } const totals = { temp: 0, humidity: 0, precip: 0 }; @@ -123,7 +123,7 @@ export default class DarkSkyWeatherProvider extends WeatherProvider { try { historicData = await httpJSONRequest( historicUrl ); } catch (err) { - throw new CodedError( ErrorCode.WeatherApiError, "An error occurred while retrieving weather information from Dark Sky." ); + throw new CodedError( ErrorCode.WeatherApiError ); } const cloudCoverInfo: CloudCoverInfo[] = historicData.hourly.data.map( ( hour ): CloudCoverInfo => { diff --git a/routes/weatherProviders/OWM.ts b/routes/weatherProviders/OWM.ts index 493929a..9aa169c 100644 --- a/routes/weatherProviders/OWM.ts +++ b/routes/weatherProviders/OWM.ts @@ -26,12 +26,12 @@ export default class OWMWeatherProvider extends WeatherProvider { forecast = await httpJSONRequest( forecastUrl ); } catch ( err ) { console.error( "Error retrieving weather information from OWM:", err ); - throw new CodedError( ErrorCode.WeatherApiError, "An error occurred while retrieving weather information from OWM." ); + throw new CodedError( ErrorCode.WeatherApiError ); } // Indicate watering data could not be retrieved if the forecast data is incomplete. if ( !forecast || !forecast.list ) { - throw new CodedError( ErrorCode.MissingWeatherField, "Necessary field(s) were missing from weather information returned by OWM." ); + throw new CodedError( ErrorCode.MissingWeatherField ); } let totalTemp = 0, @@ -112,12 +112,12 @@ export default class OWMWeatherProvider extends WeatherProvider { forecast = await httpJSONRequest( forecastUrl ); } catch (err) { console.error( "Error retrieving ETo information from OWM:", err ); - throw new CodedError( ErrorCode.WeatherApiError, "An error occurred while retrieving ETo information from OWM." ); + throw new CodedError( ErrorCode.WeatherApiError ); } // Indicate ETo data could not be retrieved if the forecast data is incomplete. if ( !forecast || !forecast.list || forecast.list.length < 8 ) { - throw new CodedError( ErrorCode.InsufficientWeatherData, "Insufficient data available from OWM." ); + throw new CodedError( ErrorCode.InsufficientWeatherData ); } // Take a sample over 24 hours. diff --git a/routes/weatherProviders/WUnderground.ts b/routes/weatherProviders/WUnderground.ts index 71114be..e83d41e 100644 --- a/routes/weatherProviders/WUnderground.ts +++ b/routes/weatherProviders/WUnderground.ts @@ -7,7 +7,7 @@ export default class WUnderground extends WeatherProvider { async getWateringData( coordinates: GeoCoordinates, pws?: PWS ): Promise< ZimmermanWateringData > { if ( !pws ) { - throw new CodedError( ErrorCode.NoPwsProvided, "WUnderground WeatherProvider requires a PWS to be specified." ); + throw new CodedError( ErrorCode.NoPwsProvided ); } const url = `https://api.weather.com/v2/pws/observations/hourly/7day?stationId=${ pws.id }&format=json&units=e&apiKey=${ pws.apiKey }`; @@ -16,7 +16,7 @@ export default class WUnderground extends WeatherProvider { data = await httpJSONRequest( url ); } catch ( err ) { console.error( "Error retrieving weather information from WUnderground:", err ); - throw new CodedError( ErrorCode.WeatherApiError, "An error occurred while retrieving weather information from WUnderground." ); + throw new CodedError( ErrorCode.WeatherApiError ); } // Take the 24 most recent observations. @@ -24,7 +24,7 @@ export default class WUnderground extends WeatherProvider { // Fail if not enough data is available. if ( samples.length !== 24 ) { - throw new CodedError( ErrorCode.InsufficientWeatherData, "Insufficient data was returned by WUnderground." ); + throw new CodedError( ErrorCode.InsufficientWeatherData ); } const totals = { temp: 0, humidity: 0, precip: 0 }; diff --git a/routes/weatherProviders/WeatherProvider.ts b/routes/weatherProviders/WeatherProvider.ts index 0074fbe..078d347 100644 --- a/routes/weatherProviders/WeatherProvider.ts +++ b/routes/weatherProviders/WeatherProvider.ts @@ -13,7 +13,7 @@ export class WeatherProvider { * does not support this method). */ getWateringData( coordinates: GeoCoordinates, pws?: PWS ): Promise< ZimmermanWateringData > { - throw new CodedError( ErrorCode.UnsupportedAdjustmentMethod, "Selected WeatherProvider does not support getWateringData" ); + throw new CodedError( ErrorCode.UnsupportedAdjustmentMethod ); } /** @@ -34,7 +34,7 @@ export class WeatherProvider { * CodedError if an error occurs while retrieving the EToData (or the WeatherProvider does not support this method). */ getEToData( coordinates: GeoCoordinates ): Promise< EToData > { - throw new CodedError( ErrorCode.UnsupportedAdjustmentMethod, "Selected WeatherProvider does not support getEToData" ); + throw new CodedError( ErrorCode.UnsupportedAdjustmentMethod ); }; /** From 1a1ecafcb02c56af3259514ac4bcdc54f9a282fa Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Mon, 2 Sep 2019 11:32:38 -0400 Subject: [PATCH 5/7] Throw errors instead of returning them --- routes/adjustmentMethods/AdjustmentMethod.ts | 3 --- .../ZimmermanAdjustmentMethod.ts | 9 ++----- routes/weather.ts | 25 +++++++++++++------ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/routes/adjustmentMethods/AdjustmentMethod.ts b/routes/adjustmentMethods/AdjustmentMethod.ts index f80a3b1..006b7d6 100644 --- a/routes/adjustmentMethods/AdjustmentMethod.ts +++ b/routes/adjustmentMethods/AdjustmentMethod.ts @@ -1,6 +1,5 @@ import { BaseWateringData, GeoCoordinates, PWS } from "../../types"; import { WeatherProvider } from "../weatherProviders/WeatherProvider"; -import { ErrorCode } from "../../errors"; export interface AdjustmentMethod { @@ -44,8 +43,6 @@ export interface AdjustmentMethodResponse { * watering. */ rainDelay?: number; - /** A code describing the type of error that occurred (if one occurred). */ - errCode?: ErrorCode; /** The data that was used to calculate the watering scale, or undefined if no data was used. */ wateringData: BaseWateringData; } diff --git a/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts b/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts index bbbc7e3..59375e1 100644 --- a/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts +++ b/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts @@ -2,7 +2,7 @@ import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from ". import { GeoCoordinates, PWS, ZimmermanWateringData } from "../../types"; import { validateValues } from "../weather"; import { WeatherProvider } from "../weatherProviders/WeatherProvider"; -import { ErrorCode } from "../../errors"; +import { CodedError, ErrorCode } from "../../errors"; /** @@ -39,12 +39,7 @@ async function calculateZimmermanWateringScale( // Check to make sure valid data exists for all factors if ( !validateValues( [ "temp", "humidity", "precip" ], wateringData ) ) { // Default to a scale of 100% if fields are missing. - return { - scale: 100, - rawData: rawData, - errCode: ErrorCode.MissingWeatherField, - wateringData: wateringData - }; + throw new CodedError( ErrorCode.MissingWeatherField ); } let humidityBase = 30, tempBase = 70, precipBase = 0; diff --git a/routes/weather.ts b/routes/weather.ts index 047f88d..c8d3e76 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -196,7 +196,7 @@ export const getWateringData = async function( req: express.Request, res: expres remoteAddress = remoteAddress.split( "," )[ 0 ]; if ( !adjustmentMethod ) { - sendWateringData( res, { errCode: ErrorCode.InvalidAdjustmentMethod } ); + sendWateringError( res, new CodedError( ErrorCode.InvalidAdjustmentMethod )); return; } @@ -210,7 +210,7 @@ export const getWateringData = async function( req: express.Request, res: expres adjustmentOptions = JSON.parse( "{" + adjustmentOptionsString + "}" ); } catch ( err ) { // If the JSON is not valid then abort the calculation - sendWateringData( res, { errCode: ErrorCode.MalformedAdjustmentOptions } ); + sendWateringError( res, new CodedError( ErrorCode.MalformedAdjustmentOptions ) ); return; } @@ -224,7 +224,7 @@ export const getWateringData = async function( req: express.Request, res: expres console.error( `An unexpected error occurred during location resolution for "${ req.url }": `, err ); } - sendWateringData( res, { errCode: codedError.errCode } ); + sendWateringError( res, codedError ); return; } @@ -241,11 +241,11 @@ export const getWateringData = async function( req: express.Request, res: expres // Make sure that the PWS ID and API key look valid. if ( !pwsId ) { - sendWateringData( res, { errCode: ErrorCode.InvalidPwsId } ); + sendWateringError( res, new CodedError( ErrorCode.InvalidPwsId ) ); return; } if ( !apiKey ) { - sendWateringData( res, { errCode: ErrorCode.InvalidPwsApiKey } ); + sendWateringError( res, new CodedError( ErrorCode.InvalidPwsApiKey ) ); return; } @@ -288,12 +288,11 @@ export const getWateringData = async function( req: express.Request, res: expres console.error( `An unexpected error occurred during watering scale calculation for "${ req.url }": `, err ); } - sendWateringData( res, { errCode: codedError.errCode } ); + sendWateringError( res, codedError ); return; } data.scale = adjustmentMethodResponse.scale; - data.errCode = adjustmentMethodResponse.errCode || 0; data.rd = adjustmentMethodResponse.rainDelay; data.rawData = adjustmentMethodResponse.rawData; @@ -309,7 +308,7 @@ export const getWateringData = async function( req: express.Request, res: expres console.error( `An unexpected error occurred during restriction checks for "${ req.url }": `, err ); } - sendWateringData( res, { errCode: codedError.errCode } ); + sendWateringError( res, codedError ); return; } } @@ -333,6 +332,16 @@ export const getWateringData = async function( req: express.Request, res: expres sendWateringData( res, data, outputFormat === "json" ); }; +/** + * Sends a response to a watering scale request with an error code and a default watering scale of 100%. + * @param res The Express Response object to send the response through. + * @param error The error code to send in the response body. + * @param useJson Indicates if the response body should use a JSON format instead of a format similar to URL query strings. + */ +function sendWateringError( res: express.Response, error: CodedError, useJson: boolean = false ) { + sendWateringData( res, { errCode: error.errCode, scale: 100 } ); +} + /** * Sends a response to an HTTP request with a 200 status code. * @param res The Express Response object to send the response through. From 3f11dbc1cc9e54092ac47a1b8fb554cdfd7ecfbe Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Mon, 2 Sep 2019 12:11:27 -0400 Subject: [PATCH 6/7] Tidy up unexpected error handling --- errors.ts | 6 +++--- routes/weather.ts | 25 +++++++------------------ 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/errors.ts b/errors.ts index 1974697..7c5d857 100644 --- a/errors.ts +++ b/errors.ts @@ -55,11 +55,11 @@ export enum ErrorCode { } /** An error with a numeric code that can be used to identify the type of error. */ -export class CodedError { +export class CodedError extends Error { public readonly errCode: ErrorCode; - public readonly message: string; - public constructor( errCode: ErrorCode ) { + public constructor( errCode: ErrorCode, message?: string ) { + super( message ); this.errCode = errCode; } } diff --git a/routes/weather.ts b/routes/weather.ts index c8d3e76..eb5b192 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -219,12 +219,7 @@ export const getWateringData = async function( req: express.Request, res: expres try { coordinates = await resolveCoordinates( location ); } catch ( err ) { - let codedError: CodedError = makeCodedError( err ); - if ( codedError.errCode === ErrorCode.UnexpectedError ) { - console.error( `An unexpected error occurred during location resolution for "${ req.url }": `, err ); - } - - sendWateringError( res, codedError ); + sendWateringError( res, makeCodedError( err ) ); return; } @@ -283,12 +278,7 @@ export const getWateringData = async function( req: express.Request, res: expres adjustmentOptions, coordinates, weatherProvider, pws ); } catch ( err ) { - let codedError: CodedError = makeCodedError( err ); - if ( codedError.errCode === ErrorCode.UnexpectedError ) { - console.error( `An unexpected error occurred during watering scale calculation for "${ req.url }": `, err ); - } - - sendWateringError( res, codedError ); + sendWateringError( res, makeCodedError( err ) ); return; } @@ -303,12 +293,7 @@ export const getWateringData = async function( req: express.Request, res: expres try { wateringData = await weatherProvider.getWateringData( coordinates ); } catch ( err ) { - let codedError: CodedError = makeCodedError( err ); - if ( codedError.errCode === ErrorCode.UnexpectedError ) { - console.error( `An unexpected error occurred during restriction checks for "${ req.url }": `, err ); - } - - sendWateringError( res, codedError ); + sendWateringError( res, makeCodedError( err ) ); return; } } @@ -339,6 +324,10 @@ export const getWateringData = async function( req: express.Request, res: expres * @param useJson Indicates if the response body should use a JSON format instead of a format similar to URL query strings. */ function sendWateringError( res: express.Response, error: CodedError, useJson: boolean = false ) { + if ( error.errCode === ErrorCode.UnexpectedError ) { + console.error( `An unexpected error occurred:`, error ); + } + sendWateringData( res, { errCode: error.errCode, scale: 100 } ); } From 6e0f9a73b830a06c785d045e4e26f072e63aacc6 Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Fri, 6 Sep 2019 15:36:58 -0400 Subject: [PATCH 7/7] Fix bug causing all errors to be treated as unexpected --- errors.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/errors.ts b/errors.ts index 7c5d857..d73aa17 100644 --- a/errors.ts +++ b/errors.ts @@ -60,6 +60,8 @@ export class CodedError extends Error { public constructor( errCode: ErrorCode, message?: string ) { super( message ); + // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf( this, CodedError.prototype ); this.errCode = errCode; } }