-
Notifications
You must be signed in to change notification settings - Fork 399
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
Fix #1358 Expose common utilities for building HTTP module based receivers #1359
Conversation
Codecov Report
@@ Coverage Diff @@
## main #1359 +/- ##
==========================================
+ Coverage 72.69% 77.12% +4.42%
==========================================
Files 17 17
Lines 1465 1460 -5
Branches 437 432 -5
==========================================
+ Hits 1065 1126 +61
+ Misses 310 236 -74
- Partials 90 98 +8
Continue to review full report at Codecov.
|
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.
comments for reviewers
ReceiverUnhandledRequestHandlerArgs, | ||
} from '@slack/bolt'; | ||
|
||
// TODO: import from @slack/oauth |
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 issue will be resolved by slackapi/node-slack-sdk#1446
|
||
const verifyErrorPrefix = 'Failed to verify authenticity'; | ||
|
||
export class HTTPModuleFunctions { |
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.
Just to have namespace to organize these functions, I've added this class and put all the functions as static methods to the class. I know some people may not prefer this style but importing a bunch of functions can be even harder. If you have a different idea to easily manage and use a large set of functions, let me know that!
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.
🙋 person who does not prefer this style 😆
In all seriousness, I'm fine with it. Class-with-static-functions is basically the same thing as exporting an object of functions so it doesn't matter much to me. If we can avoid using instance-based class semantics (and particularly side effects), I think that makes maintainability and readability easier, so that is what is important to me.
} | ||
} | ||
|
||
export interface RequestVerificationOptions { |
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.
Renamed this but the previous type name was not exposed to external code.
} | ||
|
||
// which handles errors occurred while dispatching a rqeuest | ||
export interface ReceiverDispatchErrorHandlerArgs { |
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.
Renamed this a bit (just removed HTTP
prefix) but the previous type name was not exposed to external code.
} | ||
}, this.unhandledRequestTimeoutMillis); | ||
|
||
const ack = new HTTPResponseAck({ |
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.
Some people may not like the idea to have a class object with state in general but we have many state / local variables (isAcknowledged, storedResponse, bufferedReq, res) here. Managing them in a single place should not be a bad idea.
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.
While I prefer functional to object-oriented approach I really like it used for this situation! Inheritance for receivers and encapsulating shared logic in these classes makes a lot of sense to me.
@@ -0,0 +1,88 @@ | |||
import { Logger } from '@slack/logger'; |
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.
TODO: write some simple unit tests for this class
src/receivers/HTTPResponseAck.ts
Outdated
}; | ||
} | ||
|
||
public markAsAcknowledged(): void { |
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.
naming suggestion?
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.
ack()
? acknowledge()
? ACK()
?
P.S. I'm bad at naming, don't listen to me.
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.
Initially, I also named this as ack()
but came to think that this method can be more specific about what the methods actually does. That being said, using ack()
for simplicity here may be fine.
@@ -1,5 +1,6 @@ | |||
import { IncomingMessage } from 'http'; | |||
|
|||
// Deprecated: this function will be removed in the near future | |||
export function extractRetryNum(req: IncomingMessage): number | undefined { |
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 didn't remove these methods this time. These methods are no longer used. Also, they are not exported from src/index.ts
. Thus, deleting this whole file should be safe enough.
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.
Perhaps we should add some kind of warning log to these deprecated exported methods? Otherwise for existing users upgrading this would be invisible.
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.
Great idea. Will add some logs
@@ -0,0 +1,335 @@ | |||
/* eslint-disable node/no-extraneous-import */ |
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'm going to add one more example (perhaps, fastify) here. I'm planning that these receivers can be eventually released as optional npm packages.
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.
Wow what a great addition! The KoaReceiver is such a great instructional tool, too. I wish this existed when I started at Slack, it would have really helped me understand how Bolt works.
Very nicely done!
I think there will be a lot of documentation work related to this PR that would be an excellent reference for devs. Not just API reference docs for the shared methods you encapsulated into utilities for devs to use in their own receivers, but also I think a kind of tutorial or walk-through doc showing devs how to write a custom receiver from scratch (i.e. taking this existing document and turning it into a larger, more robust tutorial document).
} catch (err) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const e = err as any; | ||
this.logger.warn(`Request verification failed: ${e.message}`); |
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, in the case that signature verification is disabled, would this be the right error message to surface?
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 error message was taken from HTTPReceiver - we can improve both
installationStore?: InstallProviderOptions['installationStore']; // default MemoryInstallationStore | ||
scopes?: InstallURLOptions['scopes']; | ||
installerOptions?: InstallerOptions; | ||
koa: Koa; |
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.
What do you think about making the koa
and router
properties here optional?
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.
Good point - I will make this more flexible later
@@ -264,55 +264,25 @@ export default class ExpressReceiver implements Receiver { | |||
} | |||
|
|||
private async requestHandler(req: Request, res: Response): Promise<void> { |
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.
Wow what a huge improvement!
|
||
const verifyErrorPrefix = 'Failed to verify authenticity'; | ||
|
||
export class HTTPModuleFunctions { |
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.
🙋 person who does not prefer this style 😆
In all seriousness, I'm fine with it. Class-with-static-functions is basically the same thing as exporting an object of functions so it doesn't matter much to me. If we can avoid using instance-based class semantics (and particularly side effects), I think that makes maintainability and readability easier, so that is what is important to me.
} | ||
}, this.unhandledRequestTimeoutMillis); | ||
|
||
const ack = new HTTPResponseAck({ |
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.
While I prefer functional to object-oriented approach I really like it used for this situation! Inheritance for receivers and encapsulating shared logic in these classes makes a lot of sense to me.
src/receivers/HTTPResponseAck.ts
Outdated
}; | ||
} | ||
|
||
public markAsAcknowledged(): void { |
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.
ack()
? acknowledge()
? ACK()
?
P.S. I'm bad at naming, don't listen to me.
@@ -1,5 +1,6 @@ | |||
import { IncomingMessage } from 'http'; | |||
|
|||
// Deprecated: this function will be removed in the near future | |||
export function extractRetryNum(req: IncomingMessage): number | undefined { |
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.
Perhaps we should add some kind of warning log to these deprecated exported methods? Otherwise for existing users upgrading this would be invisible.
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.
Hey there! Very excited for this PR 😄
Currently looking through the code to use before these functions are available in the package, and noticed a couple of things that stood out to me. Hope these comments are helpful.
src/receivers/HTTPModuleFunctions.ts
Outdated
const hmac = createHmac('sha256', signingSecret); | ||
hmac.update(`${signatureVersion}:${requestTimestampSec}:${bufferedReq.rawBody.toString()}`); | ||
const ourSignatureHash = hmac.digest('hex'); | ||
if (!tsscmp(signatureHash, ourSignatureHash)) { |
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.
Does this throw a type error? signatureHash
could be undefined.
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.
Probably? We can add more tests to check this
src/receivers/HTTPModuleFunctions.ts
Outdated
public static getHeader(req: IncomingMessage, header: string): string { | ||
const value = req.headers[header]; | ||
if (value === undefined || Array.isArray(value)) { | ||
throw new Error(`${verifyErrorPrefix}: header ${header} did not have the expected type (${value})`); |
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.
inside of this conditional we know that value
will be either undefined
or array
which is not the expected type to insert into the formatted string.
I think that this message also is not entirely clear about the error as is.
What would you think about something like ... the expected type (received ${typeof value}, expected string)
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.
Sounds good 👍 I would like to avoid changing these functions a lot in this PR but error message improvement would be safe enough.
Before merging this pull request, I am going to work on the following changes:
|
d8cf79f
to
82eda54
Compare
Co-authored-by: Fil Maj <[email protected]>
Co-authored-by: Fil Maj <[email protected]>
4faf1b6
to
1c815d9
Compare
Summary
This pull request resolves #1358 by extracting many parts of the
HTTPReceiver
and enabling them to use for any external code. I will add a few in-line comments for reviewers. The newly adde Koa.js based receiver implementation demonstrates how to use the extracted common modules outside the bolt-js core code.Requirements (place an
x
in each[ ]
)