How do I access the raw body of a Fastify request? - javascript

As you may imagine, I'm familiar with Express, but this is the first time I'm using Fastify.
I'd like to access the unmodified body of a Fastify request, for signature verification of a webhook - ie, I would like to see the request as it came in, unmodified by any middleware. In Express this is often done by accessing request.rawBody.
How do I access the raw body of a Fastify request?

You can also check this community plugin: https://github.com/Eomm/fastify-raw-body
If you are using Typescript & fastify/autoload, place this to plugins/rawbody.ts:
import fp from "fastify-plugin";
import rawbody, { RawBodyPluginOptions } from "fastify-raw-body";
export default fp<RawBodyPluginOptions>(async (fastify) => {
fastify.register(rawbody, {
field: "rawBody", // change the default request.rawBody property name
global: false, // add the rawBody to every request. **Default true**
encoding: "utf8", // set it to false to set rawBody as a Buffer **Default utf8**
runFirst: true, // get the body before any preParsing hook change/uncompress it. **Default false**
routes: [], // array of routes, **`global`** will be ignored, wildcard routes not supported
});
});
Since global:false We need to configure it in the specific handler:
fastify.post<{ Body: any }>(
"/api/stripe_webhook",
{
config: {
// add the rawBody to this route. if false, rawBody will be disabled when global is true
rawBody: true,
},
},
async function (request, reply) {
...
Then you can access raw body in the handler using request.rawBody

Note: Fastify requests can only have req.body - they can't have, for example, req.body and req.rawBody like other web servers (for example, Express). This is because addContentTypeParser() only returns a modified body, it can't add anything else to the request.
Instead, in a content type parser we only add to one route, we make:
req.body.parsed (the object, same content that would normally be in req.body)
req.body.raw (the string)
See GitHub and the addContentTypeParser() docs for more.
server.addContentTypeParser(
"application/json",
{ parseAs: "string" },
function (req, body, done) {
try {
var newBody = {
raw: body,
parsed: JSON.parse(body),
};
done(null, newBody);
} catch (error) {
error.statusCode = 400;
done(error, undefined);
}
}
);

There's an issue on GitHub for rawBody support
And there's a module too: "raw-body". To use this module in Fastify:
const rawBody = require('raw-body')
fastify.addContentTypeParser('*', (req, done) => {
rawBody(req, {
length: req.headers['content-length'],
limit: '1mb',
encoding: 'utf8', // Remove if you want a buffer
}, (err, body) => {
if (err) return done(err)
done(null, parse(body))
})
})
I hope that I helped you, I'm new on fastify too

Related

Cypress: remove all cookies with intercepted route

I'm intercepting my login and logout routes in my functional tests with Cypress. (I have to stub them because the Magic technology I'm using for authentication does NOT support a test mode for the server side SDK, yet.)
Here is the code for the routes:
import {
loginRoute,
logoutRoute,
} from 'features/user-authentication/user-authentication-api';
// ...
cy.intercept(loginRoute, request => {
request.reply({
headers: {
'Set-Cookie': `magic-auth-token=${Cypress.env(
'validMagicAuthToken',
)}`,
},
statusCode: 200,
body: { success: true },
});
});
cy.intercept(logoutRoute, request => {
request.reply({
headers: {
'Set-Cookie': `magic-auth-token=; Max-Age=-1; Path=/`,
},
statusCode: 302,
});
});
I'm mimicking the original route's behavior, where they add and remove cookies. The login route's stub works perfectly. However, the stub for the login route does not.
The original logout route looks like this:
import { parse, serialize } from 'cookie';
// ...
function removeTokenCookie<T>(response: NextApiResponse<T>) {
const cookie = serialize(TOKEN_NAME, '', {
maxAge: -1,
path: '/',
});
response.setHeader('Set-Cookie', cookie);
}
const logoutHandler: NextApiHandler = async (request, response) => {
const session = await getSession(request);
if (session) {
await magic.users.logoutByIssuer(session.issuer);
}
removeTokenCookie(response);
response.writeHead(302, { Location: '/' });
response.end();
};
How can I remove the cookies using the logout route's stub? For some reason the cookie does NOT get removed when I set the headers as I did above.
Cypress has the clearCookie command, but it can't be used inside the intercept callback.
cy.intercept(logoutRoute, request => {
cy.clearCookie('magic-auth-token')
request.reply...
})
This is the error
CypressError
Cypress detected that you returned a promise from a command while also invoking one or more cy commands in that promise.
The cy command you invoked inside the promise was: cy.clearCookie()
Looking at the source code for clearCookie, it boils down to the internal command
Cypress.automation('clear:cookie', { name: <cookie-name> })
While it's an internal command, it's use has been demo'd here Cypress Automation and here Testing an Application in Offline Network Mode
The type definitions were added recently Add type for Cypress.automation #7573
Here's a proof of concept,
it('clears cookies in intercept', () => {
cy.setCookie('magic-auth-token', '1234')
cy.getCookies().should('have.length', 1)
cy.intercept('*', (req) => {
Cypress.automation('clear:cookie', { name: 'magic-auth-token' })
})
cy.visit('http://example.com').then(() => {
// after the request has been intercepted
cy.getCookies().should('have.length', 0)
})
})

What extra fields does the AWS Amplify API include when invoking a Lambda function?

I have this AWS Lambda function to create a note object in my DynamoDB table:
import * as uuid from "uuid";
import AWS from "aws-sdk";
const dynamoDb = new AWS.DynamoDB.DocumentClient();
export function main(event, context, callback) {
// Request body is passed in as a JSON encoded string in 'event.body'
const data = JSON.parse(event.body);
const params = {
TableName: process.env.tableName,
// 'Item' contains the attributes of the item to be created
// - 'userId': user identities are federated through the
// Cognito Identity Pool, we will use the identity id
// as the user id of the authenticated user
// - 'noteId': a unique uuid
// - 'content': parsed from request body
// - 'attachment': parsed from request body
// - 'createdAt': current Unix timestamp
Item: {
userId: event.requestContext.identity.cognitoIdentityId,
noteId: uuid.v1(),
content: data.content,
attachment: data.attachment,
createdAt: Date.now()
}
};
dynamoDb.put(params, (error, data) => {
// Set response headers to enable CORS (Cross-Origin Resource Sharing)
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": true
};
// Return status code 500 on error
if (error) {
const response = {
statusCode: 500,
headers: headers,
body: JSON.stringify({ status: false })
};
callback(null, response);
return;
}
// Return status code 200 and the newly created item
const response = {
statusCode: 200,
headers: headers,
body: JSON.stringify(params.Item)
};
callback(null, response);
});
}
What it does is not really relevant to the question. The important thing to note here is that it can be successfully executed offline with the command serverless invoke local --function create --path mocks/create-event.json and an example event create-event.json:
{
"body": "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}",
"requestContext": {
"identity": {
"cognitoIdentityId": "USER-SUB-1234"
}
}
}
However, when I invoke this Lambda function with a POST request through the aws-amplify API, I only define a body field in the init object, i.e.
import { API } from "aws-amplify";
...
function createNote(note) {
return API.post("scratch-notes", "/scratch-notes", {
body: note
});
}
Which leads to the following questions...
How did the Lambda function get the needed requestContext field
from the aws-amplify API POST method that invoked it?
Was it appended to the init object by the aws-amplify API?
That would be the obvious answer, but that leads me to another question...
What other fields are appended to the init object by the aws-amplify API?
The event object also contains two extra structures, afaik: a requestContext object and a pathParameters object.
The requestContext object is just the context object included in the event object for testing purposes and for our general convenience.
The pathParameters object is a list of fields that are extracted from paths with special tokens.
get:
handler: get.main
events:
- http:
path: scratch-notes/{id}
method: get
cors: true
authorizer: aws_iam
A handler like that would take a request with the URI scratch-notes/1234 and return a pathParameters object as follows:
{
"pathParameters": {
"id": "1234"
}
}
Of course, both objects can be combined to invoke our API offline with the Serverless framework which would look like this:
{
"pathParameters": {
"id": "59747e00-8e61-11ea-8cb8-5d9bcedbe6f4"
},
"body": "myBody",
"requestContext": {
"identity": {
"cognitoIdentityId": "USER-SUB-1234"
}
}
}

Can't use #Res() with FilesInterceptor()

I am trying to upload a file using builtin multer and after then sending the response back to the user for success or failure. It was all going good until today, when I try to upload the Response wont come. after digging a bit I find out that when i use #res with #UploadedFile it does not execute the controller. I am new to nest.js.
Working.
#Post('uploads/avatar')
async uploadFile(#Req() req, #UploadedFile() avatar) {
console.log(req.body);
if (!req.body.user_id) {
throw new Error('id params not found.');
}
try {
const resultUpload = await this.userService.uploadUserImage(
req.body.user_id,
avatar,
); // returns the url for the uploaded image
return resultUpload;
} catch (error) {
console.log(error);
return error;
}
}
Not Working.
#Post('uploads/avatar')
async uploadFile(#Req() req, #UploadedFile() avatar, #Res() res) {
console.log(req.body);
if (!req.body.user_id) {
throw new Error('id params not found.');
}
try {
const resultUpload = await this.userService.uploadUserImage(
req.body.user_id,
avatar,
); // returns the url for the uploaded image
return resultUpload;
res.send(resultUpload);
} catch (error) {
console.log(error);
res.send(error);
}
}
In nest, you should always avoid injecting #Res because then you lose a lot of things that make nest so great: interceptors, exception filters,...
And actually, in most cases you don't need #Res since nest will automatically handle sending the response correctly.
If you want to send data from a controller method, you can just return the data (Promises and Observables will be resolved automatically as well). If you want to send an error to the client, you can just throw the corresponding HttpException, e.g. 404 -> NotFoundException:
#Post('uploads/avatar')
async uploadFile(#Req() req, #UploadedFile() avatar) {
if (!req.body.user_id) {
// throw a 400
throw new BadRequestException('id params not found.');
}
try {
const resultUpload = await this.userService.uploadUserImage(
req.body.user_id,
avatar,
);
return resultUpload;
} catch (error) {
if (error.code === 'image_already_exists') {
// throw a 409
throw new ConflictException('image has already been uploaded');
} else {
// throw a 500
throw new InternalServerException();
}
}
}
If for some reason you have to inject #Res here, you cannot use the FilesInterceptor. Then you have to configure the multer middleware yourself.
Side note
You can create a custom decorator for accessing the userId:
import { createParamDecorator } from '#nestjs/common';
export const UserId = createParamDecorator((data, req) => {
if (!req.body || !req.body.user_id) {
throw new BadRequestException('No user id given.')
}
return req.body.user_id;
});
and then use it in your controller method like this:
#Post('uploads/avatar')
async uploadFile(#UserId() userId, #UploadedFile() avatar) {
look, when you are using an interceptor, you are handling (with using .handle()) the stream of response (observable) not a whole package of it, but using express #Res actually is somehow getting around the whole flow of response streaming.
this is also explicitly mentioned in nestjs official documents:
We already know that handle() returns an Observable. The stream
contains the value returned from the route handler, and thus we can
easily mutate it using RxJS's map() operator.
WARNING
The response mapping feature doesn't work with the
library-specific response strategy (using the #Res() object directly
is forbidden).

How to post raw body using the npm request package

I want to post a request to a service using the npm request package. The service accepts a request body of comma-separated numbers such as 1,2,3, but when I post the request, the body is "1,2,3" instead, which the service does not recognise. I use the following code:
import request from "request";
export function getIt(numbers) {
return new Promise((resolve, reject) => {
const uri = 'http://foo.bar/getIt';
const numbersBody = numbers.join(',')
console.log('post body: ', numbersBody) // prints 1,2,3
request
.post(
uri,
{ json: true, rejectUnauthorized: false, body: numbersBody },
(err, httpResponse, body) => {
let error = err && err.message ? err.message : err;
if (body && body._error_code) {
error = body.message;
}
if (error) {
reject(error);
} else {
resolve(body);
}
}
)
});
When I inspect the network traffic with Wireshark, I can see that the body becomes "1,2,3". It seems to be because the request package writes it as a json object. I have tried to remove the json: true part of the option, and also tried changing it to false, but that throws an exception. So I don't know how to make the string be the raw body, without the quotation marks. Is there any way to make that happen?

Consume JSON from a URL in Express

The question is: How can I import json from a URL specifically, NOT an internal file in Express, and contain it such that I can use it across multiple views. For example, I have a controller. How can I get in in there (controller)? I am using request.
I have a router with 2 routes but I want to have a bunch more, and the bulk of the logic for the routes is being done in controllers.
Below is a controller with the route for showing all. I had hardcoded a small piece of "json" in it as data to use temporarily, but now I want to populate my view via an outside api. This is my controller:
module.exports = {
//show all USERS
showDogs: (req,res) => {
const dogs = [
{
name:"Fluffy", breed:"ChowChow", slug:"fluffy", description:"4 year old Chow. Really, really fluffy."
},
{
name:"Buddy", breed:"White Lab", slug:"buddy", description:"A friendly 6 year old white lab mix. Loves playing ball"
},
{
name: "Derbis", breed:"Schmerbis",slug:"derbis", description:"A real Schmerbis Derbis"
}
];
res.render("pages/dogs", {dogs: dogs, title:"All Dogs"});
}
};
How can I get this json the data to come from an outside line? I have used request before but I don't know how to transfer the data between files. I don't want to put it inside the showDogs or it won't be accessible to other functions here. Right?
I had something like this below, with require('request') at the top of the controller, but it just gave errors.
const options = {
url:'https://raw.githubusercontent.com/matteocrippa/dogbreedjsondatabase/master/dog-breed.json',
method:'GET',
headers:{
'Accept-Charset': "utf-8"
NO IDEA ABOUT THIS AREA FOR NOW EITHER
}
I also tried wrapping the entire thing, all the functions, in a request:
request('https://raw.githubusercontent.com/matteocrippa/dogbreedjsondatabase/master/dog-breed.json', function(error, response, body)
But still I got an error.
And this the route.js where the controller sends:
//dogs
router.get('/dogs', dogsController.showDogs)
I am a Node beginner so the only thought I have is to write some middleware. The deeper problem here is I don't know how to use/write middleware properly. Perhaps I can become informed.
Add a utility file that contains the code to talk to the external API. Include this file and use it's function to get dogs data. Later, you can add more functions for other APIs as well.
const getDogData = require('../externalApis').getDogData;
module.exports = {
//show all USERS
showDogs: (req, res) => {
getDogData(function(err, dogs) {
if (err) {
//handle err
} else {
res.render("pages/dogs", {
dogs: dogs,
title: "All Dogs"
});
}
}
}
};
// externalApis.js
const request = require ('request');
module.exports = {
getDogData: function(done) {
const options = {
url: 'https://raw.githubusercontent.com/matteocrippa/dogbreedjsondatabase/master/dog-breed.json',
method: 'GET',
headers: {
'Accept-Charset': "utf-8"
}
}
request(options, function(error, response, body) {
if (error) {
return done(err);
} else {
var data = JSON.parse(body); // not sure how's data is returned or if it needs JSON.parse
return done(null, data.dogs); //return dogs
}
});
}

Categories

Resources