-
Notifications
You must be signed in to change notification settings - Fork 0
PM-1073 trolley webhook handling #28
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
Conversation
end_of_line = lf | ||
charset = utf-8 | ||
trim_trailing_whitespace = true | ||
insert_final_newline = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure there is a newline at the end of the file. This is a common convention to avoid potential issues with some tools and version control systems.
CREATE TABLE "trolley_webhook_log" ( | ||
"id" UUID NOT NULL DEFAULT uuid_generate_v4(), | ||
"event_id" TEXT NOT NULL, | ||
"event_time" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider specifying the timezone for the TIMESTAMP fields to ensure consistency across different environments. Use TIMESTAMP WITH TIME ZONE if appropriate.
prisma/migrations/20250416100509_add_trolley_webhook_log_table/migration.sql
Show resolved
Hide resolved
prisma/migrations/20250416100509_add_trolley_webhook_log_table/migration.sql
Show resolved
Hide resolved
prisma/migrations/20250416100509_add_trolley_webhook_log_table/migration.sql
Show resolved
Hide resolved
@@ -7,7 +7,9 @@ import { ApiModule } from './api/api.module'; | |||
import { AppModule } from './app.module'; | |||
|
|||
async function bootstrap() { | |||
const app = await NestFactory.create<NestExpressApplication>(AppModule); | |||
const app = await NestFactory.create<NestExpressApplication>(AppModule, { | |||
rawBody: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rawBody: true
option is added to the NestFactory.create
method. Ensure that this change is necessary for handling webhook requests, as it affects how the request body is parsed. If not needed, consider removing it to avoid potential issues with request body parsing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is necessary in order to validate the wh signature. otherwise by parsing then stringifying back the payload, will break the signature. We need to work with the exact data we get from the body.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting this won't break other JSON body requests, right @vas3a ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested it, so far it works as expected
event_payload String | ||
event_model String? | ||
event_action String? | ||
status webhook_status |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure yet how we gonna use this and what will be the lifecycle of log status. Will plan this soon...
Please add new enum value in webhook_status
as logged
and default to it for status
field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kkartunov "processing" is basically "logged". When we receive the event first thing is we insert it in db and mark it as "processing".
After something handles it (if there's any handler to process it) we mark it as "processed" or "error" depending on the status of the handler.
I'll replace "processing" with "logged" to be more straight forward, sounds good?
request.rawBody?.toString('utf-8') ?? '', | ||
) | ||
) { | ||
throw new BadRequestException('Missing or invalid signature!'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be 403 Forbidden as it is more logical sounding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right!
* @throws {BadRequestException} If the signature is invalid or the webhook has already been processed. | ||
*/ | ||
@Post('trolley') | ||
async handleTrolleyWebhook(@Req() request: RawBodyRequest<Request>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is all great with approaching & preparing for event handlers.
But what we want for scope of this is something different:
handleTrolleyWebhook
need to always succeed and return 200 if we able to verify signature. Only if unable to verify signature respond with 403.- Make sure the event is logged in
trolley_webhook_log
with statuslogged
. - In case of a repeat
validateUnique
just return 200 to Trolley and do nothing on our side. - we will handle events on our own. Whatever we do with them is not Trolley bussiness but Topcoder stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true. the comment is deprecated. As mentioned in the PR, we're not throwing "processing" errors. Only missing or invalid signature will be thrown.
I'll supress the "unique" error as well - makes sense, right!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. let me rephrase that:
- yes, we're always returning 200, and 403 for invalid signature (i was also throwing 400 for duplicate calls, but will supress it)
- events are already logged as soon as they land
- ok
- exactly, that's already happening and I tried to mention it in the PR desc as well. we're on the same side 👍
@@ -0,0 +1,23 @@ | |||
-- CreateEnum | |||
CREATE TYPE "webhook_status" AS ENUM ('error', 'processed', 'logged'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The enum value 'processing' was changed to 'logged'. Ensure that this change aligns with the intended logic and that all references to 'processing' in the application code are updated accordingly to prevent any inconsistencies or errors.
enum webhook_status { | ||
error | ||
processed | ||
logged |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider whether changing the enum value from processing
to logged
accurately reflects the intended state of the webhook log. If processing
was meant to indicate an ongoing process, logged
might not convey the same meaning. Ensure that this change aligns with the logic and handling of webhook statuses throughout the application.
const requestId = headers[TrolleyHeaders.id]; | ||
|
||
try { | ||
await this.setEventState(requestId, webhook_status.logged, payload, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The event state has been changed from processing
to logged
. Ensure that this change aligns with the intended logic of the application. If the event should be marked as processing
before handling, consider revisiting this change.
const { model, action, body } = payload; | ||
const handler = this.handlers.get(`${model}.${action}`); | ||
// do nothing if there's no handler for the event (event was logged in db) | ||
if (!handler) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous implementation threw an error when no handler was found. The new implementation silently returns. Consider logging a warning or informational message to track unhandled events for monitoring purposes.
} | ||
|
||
// do not proceed any further if event has already been processed | ||
if (!(await this.trolleyService.validateUnique(request.headers))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding a log entry or some form of notification when a webhook is identified as already processed. This can help with debugging and monitoring.
return; | ||
} | ||
|
||
return this.trolleyService.handleEvent(request.headers, request.body); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The try-catch block for error handling has been removed. Consider reintroducing error handling to ensure that any exceptions thrown by handleEvent
are properly managed and logged. This will help in diagnosing issues and maintaining system stability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good.
import { | ||
Controller, | ||
Post, | ||
Req, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The BadRequestException
import has been removed but not replaced with any other exception handling for bad requests. Ensure that any necessary exception handling for bad requests is implemented elsewhere in the code.
* | ||
* @param request - The incoming webhook request containing headers, raw body, and parsed body. | ||
* @returns A success message if the webhook is processed successfully. | ||
* @throws {ForbiddenException} If the signature is invalid or the webhook has already been processed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exception type has been changed from BadRequestException
to ForbiddenException
. Ensure that this change aligns with the intended behavior of the application. If the change is intentional, verify that all parts of the application that handle this exception are updated accordingly.
request.rawBody?.toString('utf-8') ?? '', | ||
) | ||
) { | ||
throw new ForbiddenException('Missing or invalid signature!'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing the exception from BadRequestException
to ForbiddenException
may alter the intended HTTP response status code. Ensure that ForbiddenException
(HTTP 403) is the correct choice for handling missing or invalid signatures, as it indicates that the request was valid but the server is refusing action, whereas BadRequestException
(HTTP 400) indicates that the request itself is malformed. Verify that this change aligns with the desired API behavior.
https://topcoder.atlassian.net/browse/PM-1073 - Implement (trolley) Webhook handlers in Finance API
NOTE: we need to add
TROLLEY_WH_HMAC
to env. If it's missing it will throw an early error (meaning server won't start without it, just so it is obvious that we're missing it)I tried to make event handling as easy as possible, so when we start implementing them it should be quite easy:
webhooks/trolley/handlers
webhooks/trolley/index
as service providerwebhooks/trolley/handlers/payment.handler
for exampleWe also record the status of the log entry: processing, processed or error. Error state has an additional "error_message" field to log the error.
Note: events are not automatically retried if an error happens while processing. Events (webhooks) should be retried only when the system (trolley) wasn't able to reach our api. We need to make sure errors are properly handled on our side.