Skip to content
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

Merged
merged 10 commits into from
Mar 26, 2022

Conversation

seratch
Copy link
Member

@seratch seratch commented Mar 7, 2022

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 [ ])

@seratch seratch added the enhancement M-T: A feature request for new functionality label Mar 7, 2022
@seratch seratch added this to the 3.11.0 milestone Mar 7, 2022
@seratch seratch self-assigned this Mar 7, 2022
@codecov
Copy link

codecov bot commented Mar 7, 2022

Codecov Report

Merging #1359 (ad69eb4) into main (4e12423) will increase coverage by 4.42%.
The diff coverage is 57.50%.

@@            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     
Impacted Files Coverage Δ
src/errors.ts 98.11% <ø> (ø)
src/receivers/HTTPResponseAck.ts 10.34% <10.34%> (ø)
src/receivers/ExpressReceiver.ts 67.58% <25.00%> (+6.88%) ⬆️
src/receivers/HTTPReceiver.ts 59.76% <27.77%> (+8.90%) ⬆️
src/receivers/HTTPModuleFunctions.ts 77.45% <77.45%> (ø)
src/receivers/render-html-for-install-path.ts 85.71% <100.00%> (+10.71%) ⬆️
src/receivers/http-utils.ts
src/receivers/verify-request.ts

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4e12423...ad69eb4. Read the comment docs.

Copy link
Member Author

@seratch seratch left a 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
Copy link
Member Author

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 {
Copy link
Member Author

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!

Copy link
Contributor

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 {
Copy link
Member Author

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 {
Copy link
Member Author

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({
Copy link
Member Author

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.

Copy link
Contributor

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';
Copy link
Member Author

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

};
}

public markAsAcknowledged(): void {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming suggestion?

Copy link
Contributor

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.

Copy link
Member Author

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 {
Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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 */
Copy link
Member Author

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.

Copy link
Contributor

@filmaj filmaj left a 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).

examples/custom-receiver/src/KoaReceiver.ts Outdated Show resolved Hide resolved
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const e = err as any;
this.logger.warn(`Request verification failed: ${e.message}`);
Copy link
Contributor

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?

Copy link
Member Author

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

examples/custom-receiver/src/app.ts Outdated Show resolved Hide resolved
examples/custom-receiver/src/app.ts Outdated Show resolved Hide resolved
installationStore?: InstallProviderOptions['installationStore']; // default MemoryInstallationStore
scopes?: InstallURLOptions['scopes'];
installerOptions?: InstallerOptions;
koa: Koa;
Copy link
Contributor

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?

Copy link
Member Author

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> {
Copy link
Contributor

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 {
Copy link
Contributor

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({
Copy link
Contributor

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.

};
}

public markAsAcknowledged(): void {
Copy link
Contributor

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 {
Copy link
Contributor

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.

Copy link

@danerwilliams danerwilliams left a 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.

const hmac = createHmac('sha256', signingSecret);
hmac.update(`${signatureVersion}:${requestTimestampSec}:${bufferedReq.rawBody.toString()}`);
const ourSignatureHash = hmac.digest('hex');
if (!tsscmp(signatureHash, ourSignatureHash)) {

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.

Copy link
Member Author

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

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})`);

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)

Copy link
Member Author

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.

@seratch
Copy link
Member Author

seratch commented Mar 8, 2022

Before merging this pull request, I am going to work on the following changes:

  • Do more tests with HTTPReceiver and ExpressReceiver to make sure if there is no regression bug
  • Add more unit tests to cover the extracted functions
  • Add more example receivers in the examples/custom-receiver app

examples/custom-receiver/README.md Outdated Show resolved Hide resolved
examples/custom-receiver/README.md Outdated Show resolved Hide resolved
@seratch seratch force-pushed the issue-1358-http-functions branch from 4faf1b6 to 1c815d9 Compare March 26, 2022 00:25
@seratch seratch merged commit be526eb into slackapi:main Mar 26, 2022
@seratch seratch deleted the issue-1358-http-functions branch March 26, 2022 03:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement M-T: A feature request for new functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Expose common utilities for building HTTP module based receivers
3 participants