Je dois accéder au corps brut de la demande de webhook de Stripe dans mon application Nest.js.
À la suite de cet exemple, j'ai ajouté ce qui suit au module qui a une méthode de contrôleur qui nécessite le corps brut.
function addRawBody(req, res, next) { req.setEncoding('utf8'); let data = ''; req.on('data', (chunk) => { data += chunk; }); req.on('end', () => { req.rawBody = data; next(); }); } export class SubscriptionModule { configure(consumer: MiddlewareConsumer) { consumer .apply(addRawBody) .forRoutes('subscriptions/stripe'); } }
Dans le contrôleur, j'utilise @Req() req
, puis req.rawBody
pour obtenir le corps brut. J'ai besoin du corps brut car le constructEvent de l'API Stripe l'utilise pour vérifier la demande.
Le problème est que la demande est bloquée. Il semble que le req.on ne soit appelé ni pour les données ni pour l'événement de fin. Donc next()
n'est pas appelé dans le middleware.
J'ai aussi essayé d'utiliser raw-body
comme ici, mais j'ai obtenu à peu près le même résultat. Dans ce cas, le req.readable est toujours faux, donc je suis coincé là aussi.
Je suppose que c'est un problème avec Nest.js mais je ne suis pas sûr ...
4 Réponses :
J'ai rencontré un problème similaire hier soir en essayant d'authentifier un jeton Slack.
La solution que nous avons finalement utilisée nécessitait de désactiver le bodyParser de l'application Nest principale, puis de le réactiver après avoir ajouté une nouvelle clé rawBody
à la requête avec le corps de la requête brute.
const isVerified = (req) => { const signature = req.headers['x-slack-signature']; const timestamp = req.headers['x-slack-request-timestamp']; const hmac = crypto.createHmac('sha256', 'somekey'); const [version, hash] = signature.split('='); // Check if the timestamp is too old // tslint:disable-next-line:no-bitwise const fiveMinutesAgo = ~~(Date.now() / 1000) - (60 * 5); if (timestamp < fiveMinutesAgo) { return false; } hmac.update(`${version}:${timestamp}:${req.rawBody}`); // check that the request signature matches expected value return timingSafeCompare(hmac.digest('hex'), hash); }; export async function slackTokenAuthentication(req, res, next) { if (!isVerified(req)) { next(new HttpException('Not Authorized Slack', HttpStatus.FORBIDDEN)); } next(); }
Ensuite, dans mon middleware, je pourrais y accéder comme suit:
const app = await NestFactory.create(AppModule, { bodyParser: false }); const rawBodyBuffer = (req, res, buf, encoding) => { if (buf && buf.length) { req.rawBody = buf.toString(encoding || 'utf8'); } }; app.use(bodyParser.urlencoded({verify: rawBodyBuffer, extended: true })); app.use(bodyParser.json({ verify: rawBodyBuffer }));
Briller sur!
Pour tous ceux qui recherchent une solution plus élégante, désactivez bodyParser
dans main.ts
Créez deux fonctions middleware, une pour rawbody
et l'autre pour json-parsed-body
.
json-body.middleware.ts
[...] export class AppModule implements NestModule { public configure(consumer: MiddlewareConsumer): void { consumer .apply(RawBodyMiddleware) .forRoutes({ path: '/stripe-webhooks', method: RequestMethod.POST, }) .apply(JsonBodyMiddleware) .forRoutes('*'); } } [...]
raw-body.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response } from 'express'; import * as bodyParser from 'body-parser'; @Injectable() export class RawBodyMiddleware implements NestMiddleware { use(req: Request, res: Response, next: () => any) { bodyParser.raw({type: '*/*'})(req, res, next); } }
Et appliquez les fonctions middleware aux routes appropriées dans app.module.ts
.
app.module.ts
import { Request, Response } from 'express'; import * as bodyParser from 'body-parser'; import { Injectable, NestMiddleware } from '@nestjs/common'; @Injectable() export class JsonBodyMiddleware implements NestMiddleware { use(req: Request, res: Response, next: () => any) { bodyParser.json()(req, res, next); } }
BTW req.rawbody
a été supprimé d' express
depuis longtemps.
C'est la meilleure solution utilisant Nest IMHO, devrait être la réponse acceptée.
C'est la meilleure solution. Merci
C'est une excellente réponse, mais rappelez-vous de modifier l'initialisation de nestjs pour désactiver bodyParser: const app = await NestFactory.create (AppModule, {bodyParser: false,})
Incroyable! Fonctionne comme un charme une fois que vous éteignez le bodyParser intégré. Pourrait-il être ajouté à la réponse?
c'est la solution optimale.
Aujourd'hui,
car j'utilise NestJS et Stripe
J'ai installé body-parser (npm), puis dans le main.ts, il suffit d'ajouter
app.use('/payment/hooks', bodyParser.raw({type: 'application/json'}));
et il sera limité à cet itinéraire! pas de surcharge
Dans le contrôleur de hooks, ce serait quelque chose comme ce handleWebhook(@Body() raw: Buffer)
Voici mon point de vue sur l'obtention du corps brut (texte) dans le hander de NestJS:
preserveRawBodyInRequest
comme indiqué dans l'exemple JSDoc (pour restreindre uniquement le webhook stripe, utilisez "stripe-signature"
comme en-tête de filtre)RawBody
dans le gestionnaire pour récupérer le corps brut (texte)raw-request.decorator.ts:
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { NestExpressApplication } from "@nestjs/platform-express"; import { json, urlencoded } from "express"; import type { Request } from "express"; import type http from "http"; export const HTTP_REQUEST_RAW_BODY = "rawBody"; /** * make sure you configure the nest app with <code>preserveRawBodyInRequest</code> * @example * webhook(@RawBody() rawBody: string): Record<string, unknown> { * return { received: true }; * } * @see preserveRawBodyInRequest */ export const RawBody = createParamDecorator( async (data: unknown, context: ExecutionContext) => { const request = context .switchToHttp() .getRequest<Request>() ; if (!(HTTP_REQUEST_RAW_BODY in request)) { throw new Error( `RawBody not preserved for request in handler: ${context.getClass().name}::${context.getHandler().name}`, ); } const rawBody = request[HTTP_REQUEST_RAW_BODY]; return rawBody; }, ); /** * @example * const app = await NestFactory.create<NestExpressApplication>( * AppModule, * { * bodyParser: false, // it is prerequisite to disable nest's default body parser * }, * ); * preserveRawBodyInRequest( * app, * "signature-header", * ); * @param app * @param ifRequestContainsHeader */ export function preserveRawBodyInRequest( app: NestExpressApplication, ...ifRequestContainsHeader: string[] ): void { const rawBodyBuffer = ( req: http.IncomingMessage, res: http.ServerResponse, buf: Buffer, ): void => { if ( buf?.length && (ifRequestContainsHeader.length === 0 || ifRequestContainsHeader.some(filterHeader => req.headers[filterHeader]) ) ) { req[HTTP_REQUEST_RAW_BODY] = buf.toString("utf8"); } }; app.use( urlencoded( { verify: rawBodyBuffer, extended: true, }, ), ); app.use( json( { verify: rawBodyBuffer, }, ), ); }
vous n'avez probablement pas désactivé le
bodyParser
par défaut debodyParser
lors de la création de l'applicationNestApplication
dans la méthodebootsrap