Skip to content

Simplify HTTP Plugin architecture#424

Merged
heyitsaamir merged 25 commits into
mainfrom
aamirj/simlifyHttp
Mar 13, 2026
Merged

Simplify HTTP Plugin architecture#424
heyitsaamir merged 25 commits into
mainfrom
aamirj/simlifyHttp

Conversation

@heyitsaamir

@heyitsaamir heyitsaamir commented Dec 16, 2025

Copy link
Copy Markdown
Collaborator

Separate activity sending from HTTP transport layer

The previous architecture tightly coupled HTTP transport concerns with activity sending logic:

Previous Architecture:

HttpPlugin (transport) → implements ISender (sending)
                      → has send() method (creates new Client per call)
                      → has createStream() method
                      → knows about Activity protocol details

ActivityContext → depends on ISender plugin
               → cannot work without transport plugin
               → conflates transport and sending concerns

There are a few issues with this:

  • HttpPlugin created NEW Client instances on every send() call. So there's really no benefit of this logic being in the "httpclient" plugin.
  • Transport plugins (HttpPlugin) were forced to implement send/createStream. This makes it more cumbersome to build your own HttpPlugin with your own servier.
  • Users couldn't "bring their own server" without implementing ISender
  • ActivityContext was tightly coupled to plugin architecture. ("Sender" was coupled with an activity, without any necessary benefits.)

New Architecture

HttpPlugin (transport) → only handles HTTP server/routing/auth
                      → emits ICoreActivity (minimal protocol knowledge)
                      → just passes body payload to app

ActivitySender (NEW)  → dedicated class for sending activities
                     → receives injected, reusable Client
                     → handles all send/stream logic
                     → private to App class

ActivityContext       → uses ActivitySender now (which is not a plugin) 

In this PR, I am mainly decoupling responsibilities of HttpPlugin from being BOTH a listener AND a sender, to being just a listener. The sender bit is now separated to a different ActivitySender class. Other than better code organization, the main thing this lets us do is not require the app to run to be able to send proactive messages. This is a huge plus point because now the App can be used in scenarios where it doesn't necessarily need to listen to incoming messages (like agentic notifications!)

Major Decisions

1. Created ActivitySender Class

  • Centralized all activity sending logic
  • Receives reusable Client in constructor (no per-send instantiation)
  • Private to App class - internal implementation detail
  • Provides send() and createStream() methods
  • Separate from HttpPlugin

2. Introduced ICoreActivity Interface

  • Minimal fields transport layer needs: serviceUrl, id, type
  • Extensible via [key: string]: any for protocol-specific fields
  • Transport plugins work with this instead of full Activity type. So it's easier to create these.
  • Parsing to Activity happens in app.process.ts now, NOT in HttpPlugin.

3. Removed ISender Interface

  • No longer needed - plugins don't send activities
  • Plugins only handle transport (receiving requests)
  • Breaking change, but simplifies plugin architecture. This pattern wasn't documented (intentionally) because the design was subject to change. So it should be okay hopefully to change this.

Breaking Changes

For Plugin Authors:

  1. ISender removed - Custom plugins should implement IPlugin only
  2. IActivityEvent changed - Now has body: ICoreActivity instead of activity: Activity

PR Dependency Tree

This tree was auto-generated by Charcoal

@heyitsaamir heyitsaamir changed the title Aamirj/simlify http Simplify HTTP Plugin architecture Dec 16, 2025
@heyitsaamir
heyitsaamir marked this pull request as ready for review January 16, 2026 06:58
Comment thread packages/apps/src/events/activity.ts
corinagum pushed a commit that referenced this pull request Feb 12, 2026
This PR upgrades our systems from express v4 to express v5.
The changes applicable to us are fairly minimal



#### PR Dependency Tree


* **PR #424**
  * **PR #433**
    * **PR #442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
Comment thread packages/apps/src/types/event.ts Outdated
heyitsaamir added a commit that referenced this pull request Mar 11, 2026
This PR upgrades our systems from express v4 to express v5.
The changes applicable to us are fairly minimal



#### PR Dependency Tree


* **PR #424**
  * **PR #433**
    * **PR #442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
heyitsaamir added a commit that referenced this pull request Mar 12, 2026
This PR upgrades our systems from express v4 to express v5.
The changes applicable to us are fairly minimal



#### PR Dependency Tree


* **PR #424**
  * **PR #433**
    * **PR #442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
heyitsaamir added a commit that referenced this pull request Mar 12, 2026
This PR upgrades our systems from express v4 to express v5.
The changes applicable to us are fairly minimal



#### PR Dependency Tree


* **PR #424**
  * **PR #433**
    * **PR #442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
Comment thread packages/apps/src/types/plugin/sender.ts
Comment thread packages/apps/src/app.ts
Comment thread packages/botbuilder/src/plugin.ts
Comment thread packages/apps/src/app.ts
Comment thread packages/apps/src/plugins/http/plugin.ts
Comment thread packages/apps/src/events/activity.ts Outdated
Comment thread packages/apps/src/plugins/http/plugin.ts Outdated
Comment thread packages/apps/src/app.ts Outdated
Comment thread packages/apps/src/activity-sender.ts
Comment thread packages/apps/src/app.spec.ts

@corinagum corinagum left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall looks good! I think the breaking changes aspect is unfortunate but necessary, and it's not in a way that will hugely impact customers.

heyitsaamir added a commit that referenced this pull request Mar 13, 2026
This PR upgrades our systems from express v4 to express v5.
The changes applicable to us are fairly minimal



#### PR Dependency Tree


* **PR #424**
  * **PR #433**
    * **PR #442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
heyitsaamir added a commit to microsoft/teams.py that referenced this pull request Mar 13, 2026
This is a python adaptation of
microsoft/teams.ts#424.

Separate activity sending from HTTP transport layer

The previous architecture tightly coupled HTTP transport concerns with
activity sending logic:

**Previous Architecture:**
```
HttpPlugin (transport) → implements ISender (sending)
                      → has send() method (creates new Client per call)
                      → has createStream() method
                      → knows about Activity protocol details

ActivityContext → depends on ISender plugin
               → cannot work without transport plugin
               → conflates transport and sending concerns
```

There are a few issues with this:
- HttpPlugin created NEW Client instances on every send() call. So
there's really no benefit of this logic being in the "httpclient"
plugin.
- Transport plugins (HttpPlugin) were forced to implement
send/createStream. This makes it more cumbersome to build your own
HttpPlugin with your own servier.
- Users couldn't "bring their own server" without implementing ISender
- ActivityContext was tightly coupled to plugin architecture. ("Sender"
was coupled with an activity, without any necessary benefits.)

## New Architecture

```
HttpPlugin (transport) → only handles HTTP server/routing/auth
                      → emits CoreActivity (minimal protocol knowledge)
                      → just passes body payload to app

ActivitySender (NEW)  → dedicated class for sending activities
                     → receives injected, reusable Client
                     → handles all send/stream logic
                     → private to App class

ActivityContext       → uses ActivitySender now, which is not a plugin
```

In this PR, I am mainly decoupling responsibilities of HttpPlugin from
being BOTH a listener AND a sender, to being just a listener. The sender
bit is now separated to a different `ActivitySender` class. Other than
better code organization, the main thing this lets us do is **not
require the app to run to be able to send proactive messages**. This is
a huge plus point because now the App can be used in scenarios where it
doesn't necessarily need to _listen_ to incoming messages (like agentic
notifications!)

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
heyitsaamir added a commit that referenced this pull request Mar 13, 2026
This PR upgrades our systems from express v4 to express v5.
The changes applicable to us are fairly minimal



#### PR Dependency Tree


* **PR #424**
  * **PR #433**
    * **PR #442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
@heyitsaamir

heyitsaamir commented Mar 13, 2026

Copy link
Copy Markdown
Collaborator Author

This change is part of the following stack:

Change managed by git-spice.

heyitsaamir added a commit that referenced this pull request Mar 13, 2026
This PR upgrades our systems from express v4 to express v5.
The changes applicable to us are fairly minimal



#### PR Dependency Tree


* **PR #424**
  * **PR #433**
    * **PR #442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
heyitsaamir and others added 14 commits March 13, 2026 12:07
- ActivitySender now uses createTargeted/updateTargeted when isTargeted is set
- Restore default recipient logic for targeted sends in activity context
- Fix Express v5 incompatible /api* glob by scoping express.json() to route

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ait onInit, restore express.json() scope

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…CoreActivity index sig, add proactive send tests

Co-Authored-By: Claude Opus 4.6 <[email protected]>
heyitsaamir added a commit that referenced this pull request Mar 13, 2026
This PR upgrades our systems from express v4 to express v5.
The changes applicable to us are fairly minimal



#### PR Dependency Tree


* **PR #424**
  * **PR #433**
    * **PR #442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
@heyitsaamir
heyitsaamir requested a review from corinagum March 13, 2026 19:28
heyitsaamir added a commit that referenced this pull request Mar 13, 2026
This PR upgrades our systems from express v4 to express v5.
The changes applicable to us are fairly minimal

* **PR #424**
  * **PR #433**
    * **PR #442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

@lilyydu lilyydu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

Comment thread examples/proactive-messaging/README.md

@corinagum corinagum left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM! :)

@heyitsaamir
heyitsaamir merged commit bc8dbfd into main Mar 13, 2026
11 checks passed
@heyitsaamir
heyitsaamir deleted the aamirj/simlifyHttp branch March 13, 2026 22:56
heyitsaamir added a commit that referenced this pull request Mar 13, 2026
This PR upgrades our systems from express v4 to express v5.
The changes applicable to us are fairly minimal

* **PR #424**
  * **PR #433**
    * **PR #442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
heyitsaamir added a commit that referenced this pull request Mar 16, 2026
This PR upgrades our systems from express v4 to express v5.
The changes applicable to us are fairly minimal

* **PR #424**
  * **PR #433**
    * **PR #442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
heyitsaamir added a commit that referenced this pull request Mar 16, 2026
In this PR, we introduce a new object called HttpServer and begin to
deprecate HttpPlugin.

## Main changes
1. Create `HttpServer` internal class. It accepts an
`IHttpServerAdapter` which is the server implementation.
2. Pulled out the express implementation as an `IHttpServerAdapter`
3. Deprecated HttpPlugin, and made it use HttpServer + ExpressAdapter.
4. Changed BotBuilder/A2A/Mcp plugins to depend on HttpServer vs.
HttpPlugin.
5. Added examples to show how powerful IHttpServerAdapter can be with
different types of servers (hono, fastify).
6. Minor refactor of the jwt middleware such that we can reuse it in
HttpServer and app.embed.

## Why:
HTTP is a core part of our sdk. Our App object uses HTTP to set up a
server, perform auth validations, and pipe the request to the handlers
that are attached, and then return the response. Key part is that Http
is a *core* part of App, not a plugin, since core functionality is
dependent on it.
Even inside the App object, we were doing special casing for this
Http"Plugin" whereas it should never have really been a plugin to begin
with. By making it a plugin, we were exposing many non-plugin essential
things to the plugin system in general.

So what should it have been? Well, HTTP Plugin had these
responsibilities
1. Set up the express server
2. Perform validations if credentials were present
3. Pass the incoming request to App
4. Once App handlers have had a chance to process this incoming request,
pass the response back to the server.

So, we introduce a new object called `HttpServer` whose responsibilities
are essentially that ^. This object is not a plugin, but an object
that's created by App itself.

## Customization

Now this idealogical shift doesn't really warrant us doing this
refactor, but we started seeing requests from folks who wanted to hook
Teams functionality into existing servers, or replace the underlying
server infra with a non-express server. Our recommendation was to
rebuild a new HttpPlugin. But rebuilding this plugin is not simple
(since we don't really document it anywhere, and didn't expect folks to
build their own).
So `HttpServer` exposes an `IHttpServerAdapter` concept. To build the
adapter, one simply needs to build out a handler for extracting request
data, and a handler for responses. This means that you can build simple
custom adapters for your own _existing_ servers. (And if you don't pass
one in, we'll build a default express one.) Examples of servers are in
the http-adapters folder under examples/.

## Adapter Interface

The `IHttpServerAdapter` interface adapters need to implement:

```typescript
interface IHttpServerAdapter {
  registerRoute(method: HttpMethod, path: string, handler: HttpRouteHandler): void;
  serveStatic?(path: string, directory: string): void;
  start?(port: number): Promise<void>;
  stop?(): Promise<void>;
}
```

Handlers are pure functions — `({ body, headers }) → { status, body }`.
No framework-specific request/response objects leak through the
abstraction.

### Why `registerRoute`?

Some adapter patterns have the adapter own routing internally and just
receive a single callback. But our SDK creates routes dynamically —
`app.function('myFunc')` registers `/api/functions/myFunc` at runtime,
in addition to the core `/api/messages` endpoint. The adapter needs a
`registerRoute` method so that both `HttpServer` and `app.function()`
can tell it what paths to listen on.

### Optional methods

`start`/`stop` are optional — serverless adapters (Vercel, Lambda) don't
need them. `serveStatic` is optional — only needed for tab hosting.

`HttpMethod` is currently just `'POST'` (the only method the Teams
protocol uses). It may expand to a union if needed.

## Backward Compat

We've updated `HttpPlugin` to basically use `HttpServer` with an
`ExpressAdapter` internally for backward compat. I don't think this
should lead to any breaking changes (even if someone passes in their own
`HttpPlugin`). (Tested BotBuilderPlugin, from examples, and it worked
without any changes).
However, it should be noted that I marked HttpPlugin as deprecated in
this PR, so it should be discouraged going forward, and after the next
few versions, it'll be removed.

## Testing

I tested by running the following examples:

1. Echo bot
2. Devtools
3. BotBuilder
4. HttpPlugin
5. Tabs
6. AI (streaming and regular completions)



skip-test-verification (added manifest for tabs)



#### PR Dependency Tree


* **PR #424**
  * **PR #433** 👈
    * **PR #442**


#### PR Dependency Tree


* **PR #424**
  * **PR #433** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
@heyitsaamir heyitsaamir mentioned this pull request Mar 25, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants