Skip to content

DEMO-3: Add backend to our weather app #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: base-sample-app
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 61 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
const express = require('express')
const weather = require('./src/weatherAPI.js')
const {requestCityFromIP, requestCoordsFromIP} = require('./src/geoipAPI.js')
const anonymizeip = require('./src/anonymizeip.js')
var path = require('path');

const PORT = process.env.PORT || 3000
const HOST = '0.0.0.0'

// !* Edit here for demos
const RELEASE_NO = 'PROD-134'
const RELEASE_NO = 'DEMO-2'

const api = express()

Expand All @@ -14,10 +17,64 @@ api.set('view engine', 'pug');

api.use(express.static(path.join(__dirname, 'public')));

// Define the main route for the sample app. When the homepage of the sample app
// is requested, the server will first determine the client's geolocation based
// on the client's IP address.
api.get('/', (req, res) => {
res.render('index', {
release_no: RELEASE_NO
})

const clientIP = req.ip
const renderValues = {
release_no: RELEASE_NO,
ipAddr: anonymizeip(clientIP),
}

requestCityFromIP(clientIP).then((coords) => {
console.log('Coords obj:', coords)

if ("regionName" in coords && "city" in coords && "country" in coords) {
renderValues.locationName = `${coords.city}, ${coords.regionName}, ${coords.country}`
} else if ("country" in coords) {
if ("city" in coords) {
renderValues.locationName = `${coords.city}, ${coords.country}`
} else if ("region" in coords) {
renderValues.locationName = `${coords.regionName}, ${coords.country}`
} else {
renderValues.locationName = coords.country
}
} else if ("city" in coords) {
renderValues.locationName = coords.city
} else {
renderValues.locationName = coords.regionName
}

// By default, display the weather in Toronto.
return weather("43.7001", "-79.4163", "America/Toronto")
}).then((weatherData) => {
// renderValues.forecast = JSON.stringify(weatherData)
renderValues.forecast = weatherData
res.render('index', renderValues)
})
})

api.get('/geolocate', (req, res) => {
// If the geolocation permission is denied on the client side, the client
// will send a request to `/geolocate` to get the estimated coordinates
// of the client's IP address. This will then return the coordinates to the
// client, which will use them to call the weather API as it normally would.
geoip(req.ip).then((coords) => {
res.json(coords)
})
})

api.get('/weather', (req, res) => {
const queryParams = req.query
if (!queryParams.lat || !queryParams.lon || !queryParams.timezone) {
res.status(400).send('Missing query parameters. All of the following are required: lat, lon, timezone')
}

weather(queryParams.lat, queryParams.lon, queryParams.timezone).then((weatherData) => {
res.json(weatherData)
})
})

api.listen(PORT, HOST)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reviewing the provided git patch carefully, several potential issues and areas of improvement have been identified. Here are the key points:

  1. Error Handling and Promises:

    • The route /geolocate utilizes geoip(req.ip) which has not been defined or imported. The correct function from the import, requestCoordsFromIP, should likely be used here instead.
    • The .then() chaining in the '/' route does not properly propagate the promise from requestCityFromIP(clientIP) to the weather() function call. Consequently, weather() will trigger immediately instead of waiting for the requestCityFromIP to resolve. This should be managed with nested .then() or using async/await syntax for better readability and accuracy.
    • In the '/weather' route, the presence of latitude, longitude, and timezone is checked but does not filter out empty strings or invalid values. More rigorous checking (perhaps using a utility validation function) may be required to ensure robustness.
  2. Inconsistent Data Checking and Usage:

    • In the '/' route, conditions such as "region" in coords possibly intended to be "regionName" in coords, since earlier code checks for "regionName" rather than "region" which may cause a logic error.
    • In the same section, coordinate data (latitude and longitude) derived from requestCityFromIP seems unused. The default values ("43.7001", "-79.4163") for Toronto are always used in the weather() function, which makes part of the geolocation feature redundant.
  3. Potential Misuse of IP Anonymization:

    • Though anonymizeip is implemented and used to alter the clientIP before rendering it in the response, the method and thoroughness of IP anonymization are not detailed. Depending on jurisdiction, simply modifying an IP slightly may not be sufficient for anonymization from a legal perspective.
  4. Miscellaneous Issues:

    • There doesn't appear to be error handling for any of the asynchronous operations across the routes, which can lead the server to hang or crash on promise rejection (for example, failures from the weather API or geolocation API).
    • The API key or configuration setup details for the weather and geolocation services are not visible in the snippet. This makes it hard to assess if the rate limits, data usage, and respectively needed key configurations are handled properly.
      novice in some cases, an empty response could occur when an exception or reject happens in any promise chains, but no .catch() blocks are provided.
    • API modifications such as changing RELEASE_NO and the structure of route handling could impact the frontend if the frontend isn't updated concurrently to support these backend changes.

Recommendations:

  • Implement consistent and complete error handling especially for network requests.
  • Utilize async/await for clarity and ensure proper synchronization especially where data from one API is dependent on another.
  • Ensure the correct use and checking of geographical data.
  • Include comprehensive input validation for all endpoints to increase robustness.
  • Provide .catch() where promises are used and ensure all potential asynchronous errors are handled to prevent runtime errors and improve reliability.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the context of this application, how would I implement consistent and complete error handling for networked requests?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementing consistent and complete error handling for network requests in a Node.js application, especially when those requests are interacting with external APIs or services, is crucial for maintaining robustness and reliability. Here’s a comprehensive guide to enhance your current setup:

1. Use Async/Await for Clarity

Replace .then() and .catch() chains with async/await for cleaner and more readable code. This also makes error handling more straightforward with try-catch blocks.

Example Changes:
Before:

api.get('/geolocate', (req, res) => {
    geoip(req.ip).then((coords) => {
        res.json(coords);
    }).catch(err => {
        res.status(500).send('Failed to retrieve coordinates');
    });
});

After:

api.get('/geolocate', async (req, res) => {
    try {
        const coords = await geoip(req.ip);
        res.json(coords);
    } catch (err) {
        console.error(err);
        res.status(500).send('Failed to retrieve coordinates');
    }
});

2. Standardized Error Response Structure

Define a consistent structure for error messages. This helps the client applications to reliably parse and manage errors.

Example:

function sendErrorResponse(res, status, message) {
    res.status(status).json({ error: true, message });
}

3. Detailed Error Handling for Each Route

Ensure that every route has error handling that covers possible fail points, including failed network requests, invalid parameters, and any other exceptions.

Example with More Validation and Error Handling:

api.get('/weather', async (req, res) => {
    try {
        const { lat, lon, timezone } = req.query;
        if (!lat || !lon || !timezone) {
            return sendErrorResponse(res, 400, 'Missing or invalid query parameters. All of the following are required: lat, lon, timezone');
        }

        const weatherData = await weather(lat, lon, timezone);
        res.json(weatherData);
    } catch (err) {
        console.error(err);
        sendErrorResponse(res, 500, 'Failed to retrieve weather data');
    }
});

4. Handling External API Failures

When your application relies on external APIs, ensure to handle cases where the API is down, returns an unexpected response, or is slow.

Example:

try {
    const externalResponse = await externalApiCall();
    if (!externalPrevious.Response || externalResponse.status !== 200) {
        throw new Error('External API failed');
    }
    // process externalResponse
} catch (err) {
    console.error(err);
    sendErrorResponse(res, 503, 'Service unavailable');
}

5. Log Errors

Logging errors is vitally important for diagnosing problems. Using structured logging can help separate standard logs from error logs and make them easier to analyze.

Example:

catch (err) {
    console.error('Error handling request:', { route: req.path, error: err.message });
    sendErrorResponse(res, 500, 'Internal server error');
}

6. Timeouts and Retry Logic

Implement timeouts for API calls and, if appropriate, retry mechanisms to handle transient failures.

Example with Axios for HTTP Requests:

const axios = require('axios');
const axiosInstance = axios.create({
    timeout: 5000, // 5000 ms timeout
});

try {
    const response = await axiosInstance.get('https://external.api/weather');
    // handle success
} catch (error) {
    if (error.code === 'ECONNABORTED') {
        console.error('API call timed-out');
    }
    throw error;
}

By adopting these practices, you improve the application’s error handling capabilities, leading to better reliability and user experience.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show me how to add input validation to the endpoints that currently lack it?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainly! Implementing thorough input validation for your endpoints in a Node.js Express application improves security and ensures that your application handles only well-formed and expected data. I'll provide examples for the main route '/', route '/geolocate', and route '/weather' to include validation checks.

Adding Input Validation

1. Validate /weather Route Parameters

  • In your existing /weather route, there’s a lack of detailed validation for the latitude (lat), longitude (lon), and timezone (timezone). You can improve this by validating that these parameters are not only present but also correct in form, using regular expressions or a validation library like joi.

Example using joi:

const Joi = require('joi');

const weatherSchema = Joi.object({
    lat: Joi.number().min(-90).max(90).required(),
    lon: Joi.number().min(-180).max(180).required(),
    timezone: Joi.string().min(1).required() // Adjust regex according to your needs
});

api.get('/weather', async (req, res) => {
    try {
        const queryParams = req.query;
        const { error } = weatherSchema.validate(queryParams);
        if (error) {
            return res.status(400).json({ error: error.details[0].message });
        }
        
        const weatherData = await weather(queryParams.lat, queryParams.lon, queryParams.timezone);
        res.json(weatherData);
    } catch (err) {
        console.error(err);
        res.status(500).send('Failed to retrieve weather data');
    }
});

2. Validate '/geolocate' Route

  • If assumptions about the client IP format need validation (e.g., ensuring it's a valid IPv4 or IPv6 address), you can add this validation.

Example:

const Joi = require('joi');

const ipSchema = Joi.string().ip();

api.get('/geolocate', async (req, res) => {
    const { error } = ipSchema.validate(req.ip);
    if (error) {
        return res.status(400).send('Invalid IP address format');
    }

    try {
        const coords = await geoip(req.ip);
        res.json(coords);
    } catch (err) {
    console.error(err);
        res.status(500).send('Failed to retrieve coordinates');
    }
});

3. Validate '/' Main Route

  • If additional parameters are sent to the main route or if you anticipate such needs, validate appropriately. Otherwise, ensure actions such as IP anonymization are functioning correctly.

Example of checking a potentially added query parameter for demonstration:

const indexQuerySchema = Joi.object({
    someParameter: Joi.string().optional()
});

api.get('/', async (req, res) => {
    const { error } = indexQuerySchema.validate(req.query);
    if (error) {
        return res.status(400).json({ error: error.details[0].message });
    }
    
    // existing logic to handle the request...
});

General Validation Tips

  • Use Validation Libraries: Libraries like joi provide extensive options for validating JavaScript objects against a predefined schema and are very useful for handling different types of data validation efficiently.
  • Centralize Validation Logic: Consider creating middleware or helper functions that handle validation for shared parameters across different routes.
  • Error Handling: Always provide clear and actionable error messages to help client applications understand what went wrong.

By implementing these validations, you ensure that the data handling in your API endpoints is more secure and any interaction with external services is done with correctly formatted and validated data, thereby avoiding unnecessary errors or security vulnerabilities.

Expand Down
Loading