Patterns: Middlewares
botframework-webchat middleware factories: shape, curry signature, aggregation, ordering, and how to add a new middleware. Load via the
patterns-architecturecontext group.
What middlewares are
botframework-webchat exposes four extension points for customizing the rendered chat:
activityMiddleware— wraps each rendered activity (message, event, typing) with custom React.activityStatusMiddleware— wraps the status row under each message (timestamp, retry, reactions).attachmentMiddleware— wraps attachments (Adaptive Cards, files, images).cardActionMiddleware— intercepts card-action clicks (openUrl,postBack,messageBack).
Each is an array of curried functions. The library calls each function in order, threading a next continuation; a middleware can either return its own React element to render, or call next(...args) to fall through to the next middleware (and ultimately to botframework-webchat's default rendering).
File layout
Each extension point has its own factory file under src/middlewares/:
| File | Class | Wires into |
|---|---|---|
activityMiddlewares.tsx | ActivityMiddlewares | activityMiddleware prop |
activityStatusMiddlewares.tsx | ActivityStatusMiddlewares | activityStatusMiddleware prop |
attachmentMiddlewares.tsx | AttachmentMiddlewares | attachmentMiddleware prop |
cardActionMiddleware.tsx | CardActionMiddlewares | cardActionMiddleware prop |
Each file exports a single static class with two patterns:
static getAll(widget: CoreWidget): T[]— the public aggregator. Returns the ordered array of middleware functions.static privatecurry methods — one per middleware concern. Each returns the curried function to be pushed into the array.
Curry signature
The signature differs slightly per extension point; they all follow botframework-webchat's required shape.
Activity, attachment, cardAction:
() => (next: any) => (...args: any[]) => React.ReactNode | unknown
Activity status (single arg, not spread):
() => (next: any) => (args: any) => React.ReactNode | unknown
The any types are unavoidable: botframework-webchat's middleware types are loosely typed in the version we use, and tightening them would require maintaining our own .d.ts overlay.
Aggregator shape
// src/middlewares/activityMiddlewares.tsx (representative)
export default class ActivityMiddlewares {
public static getAll(widget: CoreWidget): ActivityMiddleware[] {
return [
ActivityMiddlewares.addActivityIdAttribute(widget),
ActivityMiddlewares.useUpdatedBotAvatar(widget),
ActivityMiddlewares.useAgentAvatar(widget),
ActivityMiddlewares.useCSATPopup(widget),
ActivityMiddlewares.createBackButton(widget),
ActivityMiddlewares.useRatingFormPopup(widget),
ActivityMiddlewares.createEndLiveChatButton(widget),
ActivityMiddlewares.handleMessageSound(widget),
];
}
private static addActivityIdAttribute(widget: CoreWidget) {
return () => (next: any) => (...args: any[]) => {
const [card] = args;
// ... examine activity, return wrapped JSX or call next(...args)
return next(...args);
};
}
// ... seven more
}
Wire-up
The four getAll(this) calls happen in CoreWidget.renderWebChatWithProps():
// src/widgets/coreWidget.ts (around line 883–886)
activityMiddleware: ActivityMiddlewares.getAll(this),
activityStatusMiddleware: ActivityStatusMiddlewares.getAll(this),
attachmentMiddleware: AttachmentMiddlewares.getAll(this),
cardActionMiddleware: CardActionMiddlewares.getAll(this),
These are passed as props to the CDN-loaded renderWebChat(webChatProps, domElement) function (not <ReactWebChat />; see the comment at coreWidget.ts:936 for the CDN constraint).
The widget: CoreWidget argument threads the widget instance into every middleware so handlers can access widget.botPreferences, widget.localeManager, widget.activeBotAvatar, widget.instanceStore.dispatch(...), etc.
Ordering
The order in getAll() is the order in which middlewares are evaluated. Each middleware that does not match its activity calls next(...args); the first one that returns a React element short-circuits the chain.
Current order in ActivityMiddlewares matters because of these overlaps:
addActivityIdAttributeis first — every bot message gets adata-activity-idwrapper for TTS highlight targeting.useUpdatedBotAvataranduseAgentAvatarboth wrap message activities;useUpdatedBotAvatarruns first because the live-chat agent avatar takes priority only whenwidget.activeBotAvataris unset (the live-chat decorator handles its own avatar internally).useCSATPopupanduseRatingFormPopupshort-circuit on specific event names (CSAT,webchat_showCustomRatingForm); their position beforecreateEndLiveChatButtonensures live-chat termination buttons do not appear on event activities.handleMessageSoundis last so it runs as a side effect after all rendering decisions are made.
When adding a new middleware, decide where it slots in by asking:
- Does it short-circuit (return its own JSX) or pass through (
next(...args)always)? Side-effect middlewares go at the end; rendering middlewares go where their precedence makes sense. - Does it conflict with an existing wrap? If both
useUpdatedBotAvatarand your new middleware wrap message activities, the first one wins. - Does the order matter for the
data-*attributes downstream code reads?addActivityIdAttributeis positioned first specifically because the TTS code inCoreWidgetdepends on thedata-activity-idattribute being present.
Adding a new middleware
// 1. Add a private static method to the appropriate factory class:
private static useFooDecorator(widget: CoreWidget) {
return () => (next: any) => (...args: any[]) => {
const [card] = args;
if (card.activity.channelData?.foo) {
const Wrapped = next(...args);
return ({ children }: { children?: React.ReactNode }) => (
<FooDecorator activity={card.activity}>
{Wrapped({ children })}
</FooDecorator>
);
}
return next(...args);
};
}
// 2. Push it into the array returned by getAll():
public static getAll(widget: CoreWidget): ActivityMiddleware[] {
return [
ActivityMiddlewares.addActivityIdAttribute(widget),
// ... existing entries
ActivityMiddlewares.useFooDecorator(widget), // ← new entry, position chosen per ordering rules
];
}
Rules:
- The method receives
widget: CoreWidgetso it can read state and dispatch redux actions. - Always call
next(...args)for activities that do not match — never returnnullorundefined, that breaks the chain. - The returned function is a render function (
({ children }) => JSX.Element), not the JSX element itself. botframework-webchat calls the returned function with{ children }props. - Side effects (sound, analytics) should still call
next(...args)and let the sound/analytics happen in the same closure scope.
Inventory
ActivityMiddlewares (8 middlewares)
| Name | Purpose |
|---|---|
addActivityIdAttribute | Wraps bot message activity in <div data-activity-id={id}> for TTS highlight targeting |
useUpdatedBotAvatar | If widget.activeBotAvatar is set, wraps in <BotActivityAvatarDecorator> |
useAgentAvatar | For live-chat messages, wraps in <LiveChatMessageActivityDecorator> with agent image |
useCSATPopup | On event activity named CSAT, returns <CSATPopupDecorator> |
createBackButton | If activity.channelData.showBackButton, wraps in <BackButtonDecorator> |
useRatingFormPopup | On event named webchat_showCustomRatingForm, returns <RatingFormPopupDecorator> |
createEndLiveChatButton | For active live-chat bot messages, wraps in <EndLiveChatButton> |
handleMessageSound | Plays message-received.mp3 on new bot message (side effect, then next(...)) |
ActivityStatusMiddlewares (1 middleware)
| Name | Purpose |
|---|---|
addMessageReactions | If activity.channelData.hasFeedback, renders <BotActivityStatusWithReactions> (thumbs up/down) |
AttachmentMiddlewares (2 middlewares)
| Name | Purpose |
|---|---|
applyCarouselDecorator | Wraps carousel Adaptive Cards in <CarouselDecorator>; non-carousel ones in <AdaptiveCardDecorator> |
resetFileInputAfterUpload | Clears the <input type="file"> value after a file attachment is sent |
CardActionMiddlewares (1 middleware)
| Name | Purpose |
|---|---|
cardActionLinkHandler | Intercepts openUrl actions, calls window.open(value, widget.botPreferences.linkTarget, "noopener noreferrer") |