Why my NULL check is always failing in Node.js? - javascript

I am developing a REST API with AWS Lambda, API Gateway, and Node.js.
Here is my code:
const mysql = require('mysql2');
const PropertiesReader = require('properties-reader');
const prop = PropertiesReader('properties.properties');
const con = mysql.createConnection({
host : prop.get('server.host'),
user : prop.get("server.username"),
password : prop.get("server.password"),
port : prop.get("server.port"),
database : prop.get("server.dbname")
});
exports.getMilestoneStatusByID = (event, context, callback) => {
const { id } = event.queryStringParameters;
if(id==null)
{
var response = {
"statusCode": 404,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify("Missing Parameters"),
"isBase64Encoded": false
};
callback(null, response)
}
else{
console.log("id", id);
// allows for using callbacks as finish/error-handlers
context.callbackWaitsForEmptyEventLoop = false;
const sql = "select * from milestone_status where idmilestone_status = ?";
con.execute(sql, [id], function (err, result) {
if (err) throw err;
var response = {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify(result),
"isBase64Encoded": false
};
callback(null, response)
});
}
};
In my code I am accepting a parameter called id, then get data from the database and return back as the response. If the id is not provided, i am sending an error response.
But, in any case the id is null or no parameter provided, then the user gets the standard AWS error output, "message": "Internal server error". In console, th But what I need is to pass 404 status code, with the message Missing Parameters.
e below error get printed.
Lambda returned empty body!
Invalid lambda response received: Invalid API Gateway Response Keys: {'trace', 'errorMessage', 'errorType'} in {'errorType': 'TypeError', 'errorMessage': "Cannot destructure property 'id' of 'event.queryStringParameters' as it is null.", 'trace': ["TypeError: Cannot destructure property 'id' of 'event.queryStringParameters' as it is null.", ' at Runtime.exports.getMilestoneStatusByID [as handler] (/var/task/source/milestone-status/milestonestatus-getbyid.js:17:11)', ' at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)']}
How can I implement null check and send the error that I am looking for?

The error says that event.queryStringParameters is undefined, not the .id in it. You can extend your check to also check for event.queryStringParameters, although you should check the documentation if that's even normal:
exports.getMilestoneStatusByID = (event, context, callback) => {
const params = event.queryStringParameters;
if (!params || params.id == null) {
var response = {
"statusCode": 404,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify("Missing Parameters"),
"isBase64Encoded": false
};
callback(null, response)
} else {
const { id } = params;
// ...
}

Related

How to assert an error being thrown by mocked method using Jest in AWS Lambda

I have an AWS Lambda function called getTables that runs when you hit an API Gateway endpoint.
I would like to mock an exception being thrown by a section of my code that uses de AWS Glue SDK.
How can I properly mock an exception being thrown by a method and how should I assert it?
The test that I have doesn't seem to be working cause I get an unexpected exception which indicates that another part of the code is trying to use the response of the mocked code (meaning it didn't throw?):
it('should throw on db not found', async () => {
mockGetTables.mockReturnValue(new Error("EntityNotFoundException"));
await getTables(MOCK_REQUEST_EVENT_FAIL, mockContext, mockCallback);
expect(mockCallback).rejects.toEqual('EntityNotFoundException');
});
Here's my Lambda code:
export const getTables = async (
event: APIGatewayProxyEvent,
_context: Context,
callback: Callback<APIGatewayProxyResult>
) => {
console.log('Executing /getTables Activity.');
console.log(`/getTables event: ${JSON.stringify(event)}.`);
try {
const { queryStringParameters } = event;
let catalogIdParam: string = null;
let databaseNameParam: string = null;
let maxResultsParam: number = null;
let nextTokenParam: string = null;
if (queryStringParameters) {
const { catalogId, databaseName, maxResults, nextToken } = queryStringParameters;
catalogIdParam = catalogId || null;
databaseNameParam = databaseName || null;
maxResultsParam = maxResults ? parseInt(maxResults) : null;
nextTokenParam = nextToken || null;
}
const glueClientInstance: GlueClient = GlueClient.getInstance();
//I'd like to mock the following async method and throw "EntityNotFoundException"
//just like API Gateway (or lambda) would do.
const { TableList, NextToken }: GetTablesResponse = await glueClientInstance.getTables(
databaseNameParam,
catalogIdParam,
maxResultsParam,
nextTokenParam
);
const pandaUITableList: PandaUIGlueTable[] = convertToPandaUITableList(TableList);
callback(null, {
statusCode: 200,
body: JSON.stringify({
TableList: pandaUITableList,
NextToken,
}),
});
} catch (error) {
console.log(`An error ocurred while executing /getTables activity: ${JSON.stringify(error)}`);
if (error.code === 'EntityNotFoundException') {
callback(null, {
statusCode: 400,
body: JSON.stringify({
error: error.message,
}),
});
}
//Generic/CatchAll handler that I'll test later once I figure this one out
handlerApiError(error, callback);
}
};
For reference, this is the atual error I'm trying to mock and throw:
{
"message": "Database fdv_lin not found.",
"code": "EntityNotFoundException",
"time": "2022-03-29T00:47:07.475Z",
"requestId": "fff5d84c-59de-441d-a204-e08ede830931",
"statusCode": 400,
"retryable": false,
"retryDelay": 76.52610613917457
}
You can use jest's toHaveBeenCalledWith() or toHaveBeenLastCalledWith() to assert that the callback is called with the appropriate payload in the catch clause. In this case, it is the below example.
callback(null, {
statusCode: 400,
body: JSON.stringify({
error: error.message,
}),
});
Possible Solution
const mockCallback:Callback<APIGatewayProxyResult> = jest.fn();
it('should throw on db not found', async () => {
mockGetTables.mockReturnValue(new Error('EntityNotFoundException'));
await getTables(MOCK_REQUEST_EVENT_FAIL, mockContext, mockCallback);
expect(mockCallback).toHaveBeenCalledWith(null, {
statusCode: 400,
body: JSON.stringify({
error: {
message: 'Database fdv_lin not found.',
code: 'EntityNotFoundException',
time: '2022-03-29T00:47:07.475Z',
requestId: 'fff5d84c-59de-441d-a204-e08ede830931',
statusCode: 400,
retryable: false,
retryDelay: 76.52610613917457,
},
}),
});

Node.js : How to do error handing in AWS Lambda?

I am developing a REST API with AWS Lambda, API Gateway. Language is Node.js.
Please check the below code. There I am accepting a parameter called id then get data from the database and return back as the response.
const mysql = require('mysql2');
const PropertiesReader = require('properties-reader');
const prop = PropertiesReader('properties.properties');
const con = mysql.createConnection({
host : prop.get('server.host'),
user : prop.get("server.username"),
password : prop.get("server.password"),
port : prop.get("server.port"),
database : prop.get("server.dbname")
});
exports.getMilestoneStatusByID = (event, context, callback) => {
const { id } = event.queryStringParameters;
console.log("id", id);
// allows for using callbacks as finish/error-handlers
context.callbackWaitsForEmptyEventLoop = false;
const sql = "select * from milestone_status where idmilestone_status = ?";
con.execute(sql, [id], function (err, result) {
if (err) throw err;
var response = {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify(result),
"isBase64Encoded": false
};
callback(null, response)
});
};
However, in any case the id is null or no parameter provided, then the user gets the standard AWS error output, "message": "Internal server error". But what I need is to pass 404 status code, with the message Missing Parameters.
I tried to put the whole block inside the handler into a try-catch and tried to fire the following code inside the catch
var response = {
"statusCode": 404,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify("Missing Parameters"),
"isBase64Encoded": false
};
callback(null, response)
But non of these stopped me from getting the standard AWS error message, "message": "Internal server error".
How can I implement the error that I am looking for?
I am answering my own question. The issue was not with the returning of the error, but with the null check. This worked.
exports.getMilestoneStatusByID = (event, context, callback) => {
const params = event.queryStringParameters;
if (!params || params.id == null) {
var response = {
"statusCode": 404,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify({ error: "Missing Parameters" }),
"isBase64Encoded": false
};
callback(null, response)
} else {
const { id } = params;
// ...
}

Async Lambda Function: Returning promise or sending responseURL does not terminate CloudFormation custom resource invocation

I have a lambda function invoked as a custom resource via a CloudFormation template. It Creates/Deletes AWS Connect instances. The API calls work fine but I cannot seem to terminate the custom resource invocation, so the last CF block remains CREATE_IN_PROGRESS. No matter what I return from the async function it just won't terminate the CF execution with a success.
I'm able to use a non-async handler successfully as in https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/walkthrough-custom-resources-lambda-lookup-amiids.html but I need to make multiple API calls and await completions, hence the need for async handler.
Below is the code in it's simplest form, though I've tried just about everything, including using callback and context (ie exports.handler = async function(event, context, callback) {...}), both of which should be unnecessary with an async handler. I've tried using cfn-response to directly send a response which seems to be ignored with async handlers. I've tried returning directly the promises with and without the await before them, tried returning variables containing various responseStatus and responseData, nothing seems to work.
Transform: 'AWS::Serverless-2016-10-31'
Parameters:
IdentityManagementType:
Description: The type of identity management for your Amazon Connect users.
Type: String
AllowedValues: ["SAML", "CONNECT_MANAGED", "EXISTING_DIRECTORY"]
Default: "SAML"
InboundCallsEnabled:
Description: Whether your contact center handles incoming contacts.
Type: String
AllowedValues: [true, false]
Default: true
InstanceAlias:
Description: The name for your instance.
Type: String
MaxLength: 62
OutboundCallsEnabled:
Description: Whether your contact center allows outbound calls.
Type: String
AllowedValues: [true, false]
Default: true
DirectoryId:
Description: Optional. The identifier for the directory, if using this type of Identity Management.
Type: String
ClientToken:
Description: Optional. The idempotency token. Used for concurrent deployments
Type: String
MaxLength: 500
Region:
Description: Region to place the AWS Connect Instance
Type: String
Default: us-east-1
#Handler for optional values
Conditions:
HasClientToken: !Not
- !Equals
- ""
- !Ref ClientToken
HasDirectoryId: !Not
- !Equals
- ""
- !Ref DirectoryId
Resources:
CreateConnectInstance:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "${AWS::StackName}-AWSConnectInstance"
Handler: index.handler
Runtime: nodejs12.x
Description: Invoke a function to create an AWS Connect instance.
MemorySize: 128
Timeout: 30
Role: !GetAtt LambdaExecutionRole.Arn
Layers:
- !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:layer:node_sdk:1"
Environment:
Variables:
IdentityManagementType:
Ref: IdentityManagementType
InboundCallsEnabled:
Ref: InboundCallsEnabled
InstanceAlias:
Ref: InstanceAlias
OutboundCallsEnabled:
Ref: OutboundCallsEnabled
Region:
Ref: Region
#Optional Values
ClientToken: !If
- HasClientToken
- !Ref ClientToken
- !Ref "AWS::NoValue"
DirectoryId: !If
- HasClientToken
- !Ref ClientToken
- !Ref "AWS::NoValue"
InlineCode: |
var aws = require("aws-sdk");
exports.handler = async function(event) {
console.log("REQUEST RECEIVED:\n" + JSON.stringify(event));
var connect = new aws.Connect({region: event.ResourceProperties.Region});
var isInboundCallsEnabled = (process.env.InboundCallsEnabled == 'true');
var isOutboundCallsEnabled = (process.env.OutboundCallsEnabled == 'true');
var createInstanceParams = {
InboundCallsEnabled: isInboundCallsEnabled,
OutboundCallsEnabled: isOutboundCallsEnabled,
IdentityManagementType: process.env.IdentityManagementType,
ClientToken: process.env.ClientToken,
DirectoryId: process.env.DirectoryId,
InstanceAlias: process.env.InstanceAlias
};
// Create AWS Connect instance using specified parameters
if (event.RequestType == "Create") {
return await connect.createInstance(createInstanceParams).promise();
// I can store this in a variable and read the contents fine, but...
// returning the promise does not terminate execution
}
};
InvokeCreateConnectInstance:
Type: Custom::CreateConnectInstance
Properties:
ServiceToken: !GetAtt CreateConnectInstance.Arn
Region: !Ref "AWS::Region"
The documentaiton at https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html explicitly states that you should be able to return await apiCall.promise() directly from any async function, exactly what I'm trying to do, such as
const s3 = new AWS.S3()
exports.handler = async function(event) {
return s3.listBuckets().promise()
}
Why can't I return from my async function? Again the API calls are working, the Connect instances are created and deleted (though I've omitted the delete code for brevity), but CF just hangs hours and hours until eventually saying "Custom Resource failed to stabilize in expected time"
Here's the inline code by itself for readability:
exports.handler = async function(event) {
console.log("REQUEST RECEIVED:\n" + JSON.stringify(event));
var connect = new aws.Connect({region: event.ResourceProperties.Region});
var isInboundCallsEnabled = (process.env.InboundCallsEnabled == 'true');
var isOutboundCallsEnabled = (process.env.OutboundCallsEnabled == 'true');
var createInstanceParams = {
InboundCallsEnabled: isInboundCallsEnabled,
OutboundCallsEnabled: isOutboundCallsEnabled,
IdentityManagementType: process.env.IdentityManagementType,
ClientToken: process.env.ClientToken,
DirectoryId: process.env.DirectoryId,
InstanceAlias: process.env.InstanceAlias
};
// Create AWS Connect instance using specified parameters
if (event.RequestType == "Create") {
return await connect.createInstance(createInstanceParams).promise();
// I can store this in a variable and read the contents fine, but...
// returning the promise does not terminate CF execution
}
};
UPDATE: I've implemented the sendResponse method exactly as shown in the AMI lookup example (the first link) and am sending exactly the correct structure for the response, it even includes the newly created connect instance ID in the data field:
{
"Status": "SUCCESS",
"Reason": "See the details in CloudWatch Log Stream: 2020/12/23/[$LATEST]6fef3553870b4fba90479a37b4360cee",
"PhysicalResourceId": "2020/12/23/[$LATEST]6fef3553870b4fba90479a37b4360cee",
"StackId": "arn:aws:cloudformation:us-east-1:642608065726:stack/cr12/1105a290-4534-11eb-a6de-0a8534d05dcd",
"RequestId": "2f7c3d9e-941f-402c-b739-d2d965288cfe",
"LogicalResourceId": "InvokeCreateConnectInstance",
"Data": {
"InstanceId": "2ca7aa49-9b20-4feb-8073-5f23d63e4cbc"
}
}
And STILL the custom resource will just not close in CloudFormation. I just don't understand why this is happening when I am returning the above to the event.responseURL. It's like specifying an async handler completely breaks the custom resource handler and prevents it from closing.
UPDATE: When I manually CURL the above response directly to the event.responseUrl the CF resource registers a success! WTF... I'm sending the exact same response as the lambda function is sending, and it accepts it from the CURL but not from my lambda function.
UPDATE: latest code including sendResponse, etc
var aws = require("aws-sdk");
exports.handler = async function(event, context, callback) {
console.log("REQUEST RECEIVED:\n" + JSON.stringify(event));
var connect = new aws.Connect({region: event.ResourceProperties.Region});
var isInboundCallsEnabled = (process.env.InboundCallsEnabled == 'true');
var isOutboundCallsEnabled = (process.env.OutboundCallsEnabled == 'true');
var createInstanceParams = {
InboundCallsEnabled: isInboundCallsEnabled,
OutboundCallsEnabled: isOutboundCallsEnabled,
IdentityManagementType: process.env.IdentityManagementType,
ClientToken: process.env.ClientToken,
DirectoryId: process.env.DirectoryId,
InstanceAlias: process.env.InstanceAlias
};
var responseStatus;
var responseData = {};
// Create Connect instance
if (event.RequestType == "Create") {
try {
var createInstanceRequest = await connect.createInstance(createInstanceParams).promise();
responseStatus = "SUCCESS";
responseData = {"InstanceId": createInstanceRequest.Id};
} catch (err) {
responseStatus = "FAILED";
responseData = {Error: "CreateInstance failed"};
console.log(responseData.Error + ":\n", err);
}
sendResponse(event, context, responseStatus, responseData);
return;
}
// Look up the ID and call deleteInstance.
if (event.RequestType == "Delete") {
var instanceId;
var listInstanceRequest = await connect.listInstances({}).promise();
listInstanceRequest.InstanceSummaryList.forEach(instance => {
if (instance.InstanceAlias == createInstanceParams.InstanceAlias) {
instanceId = instance.Id;
}
});
if (instanceId !== undefined) {
try {
var deleteInstanceRequest = await connect.deleteInstance({"InstanceId": instanceId}).promise();
responseStatus = "SUCCESS";
responseData = {"InstanceId": instanceId};
} catch (err) {
responseStatus = "FAILED";
responseData = {Error: "DeleteInstance call failed"};
console.log(responseData.Error + ":\n", err);
}
} else {
responseStatus = "FAILED";
responseData = {Error: "DeleteInstance failed; no match found"};
console.log(responseData.Error);
}
sendResponse(event, context, responseStatus, responseData);
return;
}
};
// Send response to the pre-signed S3 URL
function sendResponse(event, context, responseStatus, responseData) {
var responseBody = JSON.stringify({
Status: responseStatus,
Reason: "CloudWatch Log Stream: " + context.logStreamName,
PhysicalResourceId: context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData
});
console.log("RESPONSE BODY:\n", responseBody);
var https = require("https");
var url = require("url");
var parsedUrl = url.parse(event.ResponseURL);
var options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.path,
method: "PUT",
headers: {
"content-type": "",
"content-length": responseBody.length
}
};
console.log("SENDING RESPONSE...\n");
var request = https.request(options, function(response) {
console.log("STATUS: " + response.statusCode);
console.log("HEADERS: " + JSON.stringify(response.headers));
// Tell AWS Lambda that the function execution is done
context.done();
});
request.on("error", function(error) {
console.log("sendResponse Error:" + error);
// Tell AWS Lambda that the function execution is done
context.done();
});
// write data to request body
request.write(responseBody);
request.end();
}
Been at this for two days now :(
PS in the logs the "RESPONSE BODY" is shown as expected like I copied above, and log shows the "SENDING RESPONSE" but does not get to the the "STATUS: " and "HEADERS: " portion of the request.https() call, which makes me think something with async interferes with this call... IDK
This one was really tricky but finally have everything figured out. I had to make the sendResponse function asynchronous by adding a promise to it, awaiting that promise and returning it. This allowed me to ultimately call "return await sendResponse(event, context, responseStatus, responseData);" and finally everything is working, both create and delete operations are successful and the CloudFormation custom resource completes as expected. Phew. Posting code here in hopes that others will benefit from it.
var aws = require("aws-sdk");
exports.handler = async function(event, context, callback) {
console.log("REQUEST RECEIVED:\n" + JSON.stringify(event));
var connect = new aws.Connect({region: event.ResourceProperties.Region});
var isInboundCallsEnabled = (process.env.InboundCallsEnabled == 'true');
var isOutboundCallsEnabled = (process.env.OutboundCallsEnabled == 'true');
var createInstanceParams = {
InboundCallsEnabled: isInboundCallsEnabled,
OutboundCallsEnabled: isOutboundCallsEnabled,
IdentityManagementType: process.env.IdentityManagementType,
ClientToken: process.env.ClientToken,
DirectoryId: process.env.DirectoryId,
InstanceAlias: process.env.InstanceAlias
};
var responseStatus;
var responseData = {};
if (event.RequestType == "Create") {
try {
var createInstanceRequest = await connect.createInstance(createInstanceParams).promise();
responseStatus = "SUCCESS";
responseData = {"InstanceId": createInstanceRequest.Id};
} catch (err) {
responseStatus = "FAILED";
responseData = {Error: "CreateInstance failed"};
console.log(responseData.Error + ":\n", err);
}
return await sendResponse(event, context, responseStatus, responseData);
}
if (event.RequestType == "Delete") {
var instanceId;
var listInstanceRequest = await connect.listInstances({}).promise();
listInstanceRequest.InstanceSummaryList.forEach(instance => {
if (instance.InstanceAlias == createInstanceParams.InstanceAlias) {
instanceId = instance.Id;
}
});
if (instanceId !== undefined) {
try {
var deleteInstanceRequest = await connect.deleteInstance({"InstanceId": instanceId}).promise();
responseStatus = "SUCCESS";
responseData = {"InstanceId": instanceId};
} catch (err) {
responseStatus = "FAILED";
responseData = {Error: "DeleteInstance call failed"};
console.log(responseData.Error + ":\n", err);
}
} else {
responseStatus = "FAILED";
responseData = {Error: "DeleteInstance failed; no match found"};
console.log(responseData.Error);
}
return await sendResponse(event, context, responseStatus, responseData);
}
};
async function sendResponse(event, context, responseStatus, responseData) {
let responsePromise = new Promise((resolve, reject) => {
var responseBody = JSON.stringify({
Status: responseStatus,
Reason: "CloudWatch Log Stream: " + context.logStreamName,
PhysicalResourceId: context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData
});
console.log("RESPONSE BODY:\n", responseBody);
var https = require("https");
var url = require("url");
var parsedUrl = url.parse(event.ResponseURL);
var options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.path,
method: "PUT",
headers: {
"content-type": "",
"content-length": responseBody.length
}
};
console.log("SENDING RESPONSE...\n");
var request = https.request(options, function(response) {
console.log("STATUS: " + response.statusCode);
console.log("HEADERS: " + JSON.stringify(response.headers));
resolve(JSON.parse(responseBody));
context.done();
});
request.on("error", function(error) {
console.log("sendResponse Error:" + error);
reject(error);
context.done();
});
request.write(responseBody);
request.end();
});
return await responsePromise;
}
This answer is a variant on the OP's answer for those using the "ZipFile" option in the "Code" property of an AWS::Lambda::Function resource in CloudFormation. The advantage of the ZipFile approach is that in addition permitting Lambda code inlined into the CF template, it also automatically bundles a "cfn-response.js" function very similar to the "async function sendResponse" in the OP's answer. With the insight gained from the OP's answer regarding a promised response (thank you, I was stuck and perplexed), this is how I incorporated the cfn-response function as an await-able Promise to signal CF after my asynchronous AWS API calls (omitted for brevity) were complete:
CreateSnapshotFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: nodejs12.x
Handler: index.handler
Timeout: 900 # 15 mins
Code:
ZipFile: !Sub |
const resp = require('cfn-response');
const aws = require('aws-sdk');
const cf = new aws.CloudFormation({apiVersion: '2010-05-15'});
const rds = new aws.RDS({apiVersion: '2014-10-31'});
exports.handler = async function(evt, ctx) {
if (evt.RequestType == "Create") {
try {
// Query the given CF stack, determine its database
// identifier, create a snapshot of the database,
// and await an "available" status for the snapshot
let stack = await getStack(stackNameSrc);
let srcSnap = await createSnapshot(stack);
let pollFn = () => describeSnapshot(srcSnap.DBSnapshot.DBSnapshotIdentifier);
let continueFn = snap => snap.DBSnapshots[0].Status !== 'available';
await poll(pollFn, continueFn, 10, 89); // timeout after 14 min, 50 sec
// Send response to CF
await send(evt, ctx, resp.SUCCESS, {
SnapshotId: srcSnap.DBSnapshot.DBSnapshotIdentifier,
UpgradeRequired: upgradeRequired
});
} catch(err) {
await send(evt, ctx, resp.FAILED, { ErrorMessage: err } );
}
} else {
// Send success to CF for delete and update requests
await send(evt, ctx, resp.SUCCESS, {});
}
};
function send(evt, ctx, status, data) {
return new Promise(() => { resp.send(evt, ctx, status, data) });
}

Why does URLSession return null instead of json data as the response

I have this code that sends a username and password to a local API (it's http at the moment since I'm developing it still and it's not on a server yet). However the data returned is just the response code, instead of the body of the response which is meant to contain an auth-token.
This is the Client-code (my app in SwiftUI):
func login(username:String, password:String){
guard let url = URL(string: "http://localhost:3000/users/login") else { print("URL error"); return }
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
let headers = [
//"auth-token": "auth-token goes here",
"accept": "string",
"content-type": "application/json"]
let body = [
"username": username,
"password": password]
do{
let dataToSend = try JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
request.httpBody = dataToSend
}catch{
print("Error creating data object")
return
}
request.allHTTPHeaderFields = headers
request.httpMethod = "POST"
let session = URLSession.shared
let dataTask = session.dataTask(with: request) { (data, response, error) in
// Check for erros
if error == nil && data != nil{
// Try parse out data
print(data)
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
print(dictionary)
}
catch{
print("Error parsing response data")
}
}
}
dataTask.resume()
}
When I print out the data response before I try and put it in a dictionary I get this:
Optional(200 bytes)
I get the response code instead of the body of the response (which is json), why?
This is my server-side code:
router.post('/login', async (req,res) => {
// Validate before we login user
const { error } = loginValidation(req.body);
if (error) return res.status(400).send(error.details[0].message);
// Checking if the user exists
const user = await User.findOne({username: req.body.username});
if(!user) return res.status(400).send("Invalid username")
// Check if password is correct
const validPass = await bcrypt.compare(req.body.password, user.password);
if(!validPass) return res.status(400).send("Invalid password")
console.log(req.body);
// User is logged in provide them with a token
const token = jwt.sign({
_id: user._id,
_username: user.username
}, "SECRET_KEY", { expiresIn: '1h' });
res.status(200).header('auth-token', token).send(token);
});
The data doesn't seem to contain a valid JSON, it's the reason your code doesn't print out the dictionary that you tried to parse out. Here's how you find the response data:
if let data = data {
do {
let dictionary = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:Any]
print(dictionary)
} catch {
print("Error parsing response data: \(String(decoding: data, as: UTF8.self))")
}
}

Creating an Asana Task using a POST http request

I'm trying to use the asana-api to create a Task using a POST http request but I keep getting a 400 bad request as a response.
I managed to get data from the Asana-api using ( a GET request ), but I'm having trouble sending data to Asana with ( a POST request )
I'm using the 'request' module to do the api call
here's the error message :
`{"errors":[{
"message":"Could not parse request data,invalid JSON",
"help":"For more information on API status codes and how to handle them,
read the docs on errors: https://asana.com/developers/documentation/getting-started/errors"}
]}`
Here's my code:
testTask(){
var taskName = "Test Name for a Test Task"
var workspaceID = "123456789"
var projectID = "123456789"
var assigneeID = "123456789"
var parentID = null
this.createTask(taskName, workspaceID, projectID, assigneeID, parentID)
}
createTask(taskName, workspaceID, projectID, assigneeID, parentID){
var token = "0/1234abcd5678efgh9102ijk"
var bearerToken = "Bearer " + token
var task = {
data: {
assignee: "me",
notes: "test test test test",
workspace: workspaceID,
name: taskName,
projects: [projectID],
parent: parentID
}
}
var options = {
"method" : "POST",
"headers" : {"Authorization": bearerToken},
"contentType": "application/json",
"payload" : JSON.stringify(task)
}
try {
var url = "https://app.asana.com/api/1.0/tasks";
request.post(url, options, function optionalCallback(err, httpResponse, body) {
if (err) {
return console.error('upload failed:', err);
}
console.log('Upload successful! Server responded with:', body);
});
}
catch (e) {
console.log(e);
}
}
I also tried a different implementation :
createTask(){
var token = "0/1234abcd5678efgh9102ijk"
var bearerToken = "Bearer " + token
var options = {
"method" : "POST",
"headers" : {"Authorization": bearerToken},
}
try {
request.post("https://app.asana.com/api/1.0/tasks?workspace=1234567&projects=765534432&parent=null&name=taskName&assignee=me", options, function optionalCallback(err, httpResponse, body) {
if (err) {
return console.error('upload failed:', err);
}
console.log('Upload successful! Server responded with:', body);
});
}
catch (e) {
console.log(e);
}
}
Based on the examples provided by the request module, it appears that your options object uses payload as a key, but it should be body.

Categories

Resources