Server returns 404 in POST request - node.js

I have a project I build with React and Strapi headless CMS (using nodejs). The backend, which is handled using Strapi, resides in port 443. When I send a GET request to any url in backend using http://site:443 I get a valid response, but a POST request always returns an error 404.
I've used an external tool https://reqbin.com/ to test it and got the same results.
The interesting thing is that even though I get 404 response, the route is being handled and executed, and yet gives a 404 response back.
Why could this be happening?
Thanks in advance
Here is the code upon reaching this route:
/**
* Create a/an orders record.
*
* #return {Object}
*/
create: async (ctx) => {
let params = ctx.request.body;
if (params.Sum) { // only if we got a good POST response
// check secret code
let a = params.UniqueID;
var res = a.substr(a.length - 12, a.length);
if (res !== 'secret') {
ctx.throw(500, 'SECURITY BLOCK', { expose: true });
return null;
}
var username = a.substr(0, a.length - 12);
// create validity
var date = new Date();
if (params.Sum === '250') { // one month membership
date.setTime( date.getTime() + 31 * 86400000 );
} else if (params.Sum === '390') { // one month membership
date.setTime( date.getTime() + 180 * 86400000 );
}
// create order in database
let today = new Date();
// extract username
var n = params.CustomerName.indexOf("_");
var name = params.CustomerName.substr(0, n);
var instId = params.CustomerName.substr(n+1, params.CustomerName.length);
const order = await strapi.services.orders.add({
userId: username,
username: name,
institutionId: instId,
sum: params.Sum,
transactionDate: today,
validity: date,
cardNum: params.CardNum,
cardName:params.CardName,
});
let axiosArr = {
institutions: [{_id: instId}],
validity: date,
secret: 'secret'
}
axios.put(apiUrl+'/users/'+username, axiosArr).then(() => {return order})
// return order
Most of the code is irrelevant to the question, I was thinking maybe the reason that the response is 404 is because I'm not returning the order object right away, but only after the axios.put?
Unfortunately I cannot test it right now

If the route is well executed but it returns a 404, it's because the response body is empty.
In your code your commented the return order.
Add in your code something like ctx.send({order}) or un comment your return
You will no longer have a 404.

I'm not familiar with strapi but it doesn't sound like an auth issue to me. Are you sure these routes are set up to accept POST requests and not just GET requests? It's common to return 404s if a user is missing a permission needed to access a resource, to not let the person know that resource exists but without any sample code I can't really say for sure what's happening in your case. Could you link more code?

Related

How to troubleshoot socket hangup in Node app

So, I have a node app running in 2 servers. When a call is made to the app from either a browser or Postman, a load-balancer will direct the call to one of these 2 servers. I did not setup the load-balancer, so I don't currently have knowledge of how exactly it is set up.
The Node app, basically, will get a call with some data, pull a profile from Redis, then send the required data to Google Analytics. It will then return a 1x1 pixel gif back as a response, if successful, and a 400 status and a message if there is something wrong with the call like missing data or the profile key doesn't exist in Redis. I'm using Express to handle the calls and I'm using the Redis module to handle communication between the app and Redis.
Intermittently, we are seeing failed calls (no response) in the browser (Chrome and Firefox), but they will succeed in Postman. When the calls fail in the browser, I'm able to see all my logs from the app and no errors. The hits are also sometimes going to GA as we expect them to.
On occasion, I can see a failed call in Postman and the error says 'socket hang up'.
My question, really, is where should I look to find whats causing this? I'm not a network guy, and I'm kinda stumped on this.
I checked to make sure that my responses are closed correctly. I'm
using res.send() on all of my calls.
I've tested the app locally with postman and a
browser, with no issues.
I have my timeout in Postman set to 0 for
infinite, I have ssl verification turned off and I'm not using a
proxy.
I have no errors popping up in my app
We also have a ruby app that this one is replacing that does not exhibit these behaviors and I'm not sure if that's due to Ruby handling things differently than Express, or my bad programming.
Is it possible that communication between the app and Redis is getting slow and causing the socket hang up error? Or do anyone think this is more of anetworking issue?
I'm obviously not expecting anyone to be able solve this issue for me, I'm really just trying to learn and find out where I need to be looking to find out whats causing these issues.
Here is an example of one of the calls I'm making in case I'm messing this all up in the way I'm handling calls:
How I'm setting up my Redis connection:
var redis = require('redis');
var client = redis.createClient();
const { promisify } = require('util');
const hgetallAsync = promisify(client.hgetall).bind(client);
const hsetAsync = promisify(client.hmset).bind(client);
const existsAsync = promisify(client.exists).bind(client);
const deleteAsync = promisify(client.del).bind(client);
And how I'm using these to makes calls. This is a bit convoluted, but the reason is because the redis module makes Async calls and I was trying to get them to work "synchronously":
/**
* Record a pageview in GA
*
* #var {bool} exists - This returns a value of 1 if the profile exists in Redis and 0 if it does not
* #var {*} getall - This returns the profile, from Redis associated with the profile id
* #var {*} reqobj - Contains referrer, user_language, and user_agent
* #var {*} params - This is the query string from the url. Example query string: ?a=1&b=2&c=3
* #var {array} pageViewArray - [profile_id, pageView]
*/
router.get('/example.gif', function (req, res, next) {
var clientId = Before(req, res);
if (DatasetCheck(req, res)) {
var profileId = GetProfileIdFromCookie(req);
var json = {};
var params = req.query;
if (profileId) {
var exists = existsAsync(profileId);
var getall = hgetallAsync(profileId);
var validate = validation.ValidExampleRequest(clientId, req.query);
if (validate == true) {
var reqobj = RequestEnvironment(req);
var mergedobj = { ...reqobj, ...params };
var pageViewArray = PageView(mergedobj);
if (pageViewArray != false) {
exists.then(function (exists) {
if (exists > 0) {
getall.then(async function (getall) {
reqobj = RequestEnvironment(req);
var mergedobj = { ...reqobj, ...params };
PageViewPart2(mergedobj, getall, pageArray[1]);
}).then(function () {
//res.send(result);
RespondWithPixel(res);
});
} else {
logger.error("get example.gif ==> Profile does not exist");
json.status = "get example.gif ==> Profile does not exist";
res.status(400).send(JSON.stringify(json));
}
});
} else {
logger.error("get example.gif ==> Invalid hit");
json.status = "get example.gif ==> Invalid hit";
res.status(400).send(JSON.stringify(json));
}
} else {
logger.error("get example.gif ==> Invalid hit request");
json.status = "get example.gif ==> called with an empty hostname: " + params.hostname + " or path: " + params.path;
res.status(400).send(JSON.stringify(json));
}
} else {
logger.info("get example.gif ==> profileId is invalid")
json.status = "get example.gif ==> profileId is invalid or missing";
res.status(400).send(JSON.stringify(json));
}
}
});

How to use the full request URL in AWS Lambda to execute logic only on certain pages

I have a website running on www.mywebsite.com. The files are hosted in an S3 bucket in combination with cloudFront. Recently, I have added a new part to the site, which is supposed to be only for private access, so I wanted to put some form of protection on there. The rest of the site, however, should remain public. My goal is for the site to be accessible for everyone, but as soon as someone gets to the new part, they should not see any source files, and be prompted for a username/password combination.
The URL of the new part would be for example www.mywebsite.com/private/index.html ,...
I found that an AWS Lambda function (with node.js) is good for this, and it kind of works. I have managed to authenticate everything in the entire website, but I can't figure out how to get it to work on only the pages that contain for example '/private/*' in the full URL name. The lambda function I wrote looks like this:
'use strict';
exports.handler = (event, context, callback) => {
// Get request and request headers
const request = event.Records[0].cf.request;
const headers = request.headers;
if (!request.uri.toLowerCase().indexOf("/private/") > -1) {
// Continue request processing if authentication passed
callback(null, request);
return;
}
// Configure authentication
const authUser = 'USER';
const authPass = 'PASS';
// Construct the Basic Auth string
const authString = 'Basic ' + new Buffer(authUser + ':' + authPass).toString('base64');
// Require Basic authentication
if (typeof headers.authorization == 'undefined' || headers.authorization[0].value != authString) {
const body = 'Unauthorized';
const response = {
status: '401',
statusDescription: 'Unauthorized',
body: body,
headers: {
'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}]
},
};
callback(null, response);
}
// Continue request processing if authentication passed
callback(null, request);
};
The part that doesn't work is the following part:
if (!request.uri.toLowerCase().indexOf("/private/") > -1) {
// Continue request processing if authentication passed
callback(null, request);
return;
}
My guess is that the request.uri does not contain what I expected it to contain, but I can't seem to figure out what does contain what I need.
My guess is that the request.uri does not contain what I expected it to contain, but I can't seem to figure out what does contain what I need.
If you're using a Lambda#Edge function (appears you are). Then you can view the Request Event structure here: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#lambda-event-structure-request
You can see the actual value of the request URI field by using console.log and checking the respective logs in Cloudwatch.
The problem might be this line:
if (!request.uri.toLowerCase().indexOf("/private/") > -1) {
If you're strictly looking to check if a JavaScript string contains another string in it, you probably want to do this instead:
if (!request.uri.toLowerCase().indexOf("/private/") !== -1) {
Or better yet, using more modern JS:
if (!request.uri.toLowerCase().includes("/private/")) {

Strip specific query parameters in express

I am having some difficulty in stripping specific query parameters from a request in express. The issue is that in the normal flow, the user would fetch a Bearer Token from a third party which would then be sent to my REST API with each request. The token is decoded and the id of the user is added to the request.
I would like to be able to attach the ID of the user as a query parameter while in development mode, so that I can test the API using something like cURL or Postman, without having to write some sort of script to fetch a token every time.
To that end, I created a separate middleware to be used in development that would take the ID from the querystring, strip it out, and redirect to the new URL. I modified this answer to do so.
this.app.use((req, res, next) => {
const id = req.query.id;
req.user = { id };
let basePath = url.parse(req.url).pathname;
const queryKeys = Object.keys(req.query);
if(queryKeys.length === 0) return res.status(400).send('no id attached');
if(queryKeys.length === 1 && queryKeys[0] === 'id') return next();
basePath += '?';
queryKeys.forEach(queryKey => {
if(basePath[basePath.length -1] !== '?') basePath += '&';
if(queryKey !== 'id') basePath += `${queryKey}=${req.query[queryKey]}`
});
return res.redirect(basePath);
})
This function works fine if I test it using just the ID parameter, (say: http://localhost:5000/api/?id=someid) but if I add a second parameter(say: http://localhost:5000/api/?id=someid&skip=1), I get a 404 with the message / is not a recognized path.. From console.log() statements I can see that in the second example, res.redirect() is being called with /?skip=1, as expected.
What am I doing wrong?
I'm not sure I understand what you're trying to do as it seems that you're redirecting to the same URL without the id query parameter, which would return a 400 error.
In any case, please take a look at this code snippet that correctly removes the id query parameter and does the redirection.
const url = require('url');
app.use((req, res, next) => {
// return early if the id query parameter is not present
if (!req.query.id) {
return res.status(400).send('No id attached.');
}
// rebuild URL by removing the id query parameter
const baseUrl = url.format({
protocol: req.protocol,
host: req.get('host'),
});
const myUrl = new url.URL(req.originalUrl, baseUrl);
myUrl.searchParams.delete('id');
const cleanUrl = `${myUrl.pathname}${myUrl.search}`;
// redirecting to the URL without the id parameter will return a 400
return res.redirect(cleanUrl);
});
You can easily adapt the code by using the cleanUrl variable so that, instead of redirecting to it, you could take some different action.
Let me know how it goes.

Cannot read property 'getPayload' of undefined

I am implementing OAuth Google Sign in using backend (written in node.js, express framework at Heroku). Front end is Android and it sends the token id to the server just fine. And server receives the token id correctly.
Here is the code (which is ripped off straight from Google Documents)
var auth = new GoogleAuth;
var client = new auth.OAuth2(CLIENT_ID, '', '');
client.verifyIdToken(
token,
CLIENT_ID,
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3],
function(e, login) {
var payload = login.getPayload();
var userid = payload['sub'];
// If request specified a G Suite domain:
//var domain = payload['hd'];
});
But at times login in undefined. Its so strange that this problem occurs 1/10 rather than for every try so that I am not able to track the source of issue. For every other 9/10 it works just fine.
Any suggestions on how to solve this?
The problem in your code is that you are not checking if your callback get's any error.
The standard way in node.js to use a callback function is using two parameters - error is the first, the actual (success) returned data is the second, and the convention is that if an error exists - you should address it, and you're not gauranteed to get the data, and if everything went well- error will be null and you'll get your data.
So in your code, you are not checking that there's an error (and like you say, not always there's one).
Should be something like:
function(e, login) {
if (e) {
// handle error here
return; // don't continue, you don't have login
}
// if we got here, login is defined
var payload = login.getPayload();
var userid = payload['sub'];
// If request specified a G Suite domain:
//var domain = payload['hd'];
});
The first parameter to the callback function is an error that needs to handled.
function(error, login) {
if (error) return console.error(error)
var payload = login.getPayload();
var userid = payload['sub'];
// If request specified a G Suite domain:
//var domain = payload['hd'];
});

Expiring routes generation NodeJS

I want to setup temporary routes with a unique random string path on ExpressJS. These routes should be dynamically created and should give a 404 if somebody tries to use ie: http://example.com/login/a434bcd34d920bdfe 30 min after that uniqueid was created.
Any ideas on how to do that? I'm pretty new to NodeJS but judging from what I've seen there should be a library that does that :)
Something like this maybe?
// =========== app.js ============
app.get('/generate_url', function(req, res) {
// random string: "a434bcd34d920bdfe"
var extension = randomstring.generate();
var dynamicController = require('./login/'+extension);
dynamicController.init(app);
// Should expire in 20 minutes
dynamicController.expire(20*60)
res.status(200).send();
}
// =========== login.js ============
login.post('/login/:uniqueid', function(req, res) {
// uniqueid should match the extension generated before
var uniqueid = req.query.unique;
var username = req.body.username;
// Do something with this info
}
I think this is far from working fine but at least maybe somebody who's done
You asking two question:
How to create the route.
How to save the data about expired links.
1. How to create the Route:
You have to create a route that receieve the traffic from all the users, and check that the specific URL is valid.
First you create a route that get traffic from all the users.
app.get('/myroute/:id',function(){
/* This route will get any url that start with /myroute/
For example /myroute/abc
/myroute/def
*/
// req.params.id == what the URL is entered
if (is_expired(req.params.id)) return res.end('Sorry your link has expired')
res.send('Great you logged in!')
})
app.post('/login/:uniqueid', function(req, res) {
set_expire(req.params.uniquieid,30*1000*60) //30 minutes = 30*60*1000 miliseconds.
})
2. How to save the data about expired links.
How to implement set_expire and is_expired
You need to implement is using any kind of database. Redis is very good for that. I will show you example how to do it
using setTimeout. It will work. But if the server restart, all the users will be logged out.
users={}
function is_expired(uid){
return users[uid]
}
function set_expire(uid,time){
users[uid]=true
setTimeout(function(){
delete users[uid]
},time)
}
Usually this is done with a generic router handler that then consults some data store to see if the ID sent in the request is valid.
Also, note that a value that comes from a route specification like '/login/:uniqueid' is found in req.params.uniqueid, not in req.query.uniqueid. req.query is for actual query parameters (things after the ? in the URL).
var validIds = {};
// =========== app.js ============
app.get('/generate_url', function(req, res) {
// generate random string: "a434bcd34d920bdfe"
// make sure it's not already in use
var extension;
do {
extension = randomstring.generate();
} while (validIds[extension]);
// save this random string as a valid ID and store the expiration time
validIds[extension] = {expiration: Date.now() + 20 * 60 * 1000};
// return the random value to the caller
res.json(extension);
}
app.post('/login/:uniqueid', function(req, res) {
// uniqueid should match the extension generated before
var uniqueid = req.params.unique;
var username = req.body.username;
// now see if the id is still valid
var idInfo = validIds[uniqueid];
if (!idInfo || idInfo.expiration < Date.now()) {
return res.sendStatus(404);
}
// id is valid, do something with this info
}
Then, you can have some interval timer that regular cleans up any expired ids. This is just housekeeping to keep the validIds object from growing forever - the actual expiration value is still checked before validating the id.
// clean up expired validIds object every 10 minutes
setInterval(function() {
var now = Date.now();
for (var id in validIds) {
if (validIds[id].expiration < now) {
delete validIds[id];
}
}
}, 10 * 60 * 1000);
Note: Since you probably want your uniqueIDs to survive a server restart, you need to regularly persist them to some sort of backing store (database, flat file, etc...) and then you need to read that data in upon server startup.
If you want any sort of user activity to "renew" the timeout on the uniqueid, you can install some middleware that resets the time at an appropriate access.

Resources