Skip to content

Support processing HTTP requests/responses for servers created outside of HttpPlugin #345

@ryanbliss

Description

@ryanbliss

Today, there is an HttpPlugin that gets created by default. It can be overridden, bu App.http is of type HttpPlugin, implying we are expecting it to at minimum adhere to its interface. Inside of HttpPlugin, there are some assumptions baked in:

  1. The developer doesn't already have their own server (e.g., express, restify, serverless functions, etc.)
  2. The server should be started on app.start()
  3. The developer is comfortable with us setting up the routes for them

While the developer could create their own plugin to address some of these things, this is relatively unintuitive. It would be much simpler if the developer can process network requests themselves.

Here is the how this worked in Teams AI v1 (using samples that used restify):

const adapter = new TeamsAdapter(
    {},
    new ConfigurationServiceClientCredentialFactory({
        MicrosoftAppId: process.env.ENTRA_APP_CLIENT_ID,
        MicrosoftAppPassword: process.env.ENTRA_APP_CLIENT_SECRET,
        MicrosoftAppType: 'SingleTenant',
        MicrosoftAppTenantId: process.env.ENTRA_APP_TENANT_ID
    })
);

const server = restify.createServer();
server.use(restify.plugins.bodyParser());

server.listen(process.env.port || process.env.PORT || 3978, () => {
    console.log(`\n${server.name} listening to ${server.url}`);
});

server.post('/api/messages', async (req, res) => {
    // Adapter translates restify req and res into context, which the app then handles via app.run
    await adapter.process(req, res, async (context) => {
        await app.run(context);
    });
});

While verbose, the server routes were declared outside of the SDK, providing a great deal of transparency. The issue isn't that we allow handling this internally as an option, but rather that developers need to really understand how our stuff works to override this default behavior.

I propose something like the following:

const app = new App({
  clientId: process.env.ENTRA_APP_CLIENT_ID!,
  clientSecret: process.env.ENTRA_APP_CLIENT_SECRET!,
  tenantId: process.env.ENTRA_TENANT_ID!,
});

const server = restify.createServer();
server.use(restify.plugins.bodyParser());

server.listen(process.env.port || process.env.PORT || 3978, () => {
    console.log(`\n${server.name} listening to ${server.url}`);
});

// We would expose an interface that translates req / res into a common format
// We would publish for packages that use that interface for popular frameworks (e.g., Restify, Express, Next.js)
// Developers could of course create their own as well
const converter = new RestifyConverter();

server.post('/api/messages', async (req, res) => {
    // likely would want some generic interface
    await app.process(converter.convert(req, res));
});

(async () => {
  // For apps not using our standard `HttpPlugin`, they would simply initialize the SDK (which starts plugins, gets tokens, etc.)
  // `app.start()` would call `app.initialize()` internally (while also adding the `HttpPlugin`)
  await app.initialize();
})();

There are other ways we could do this, of course. For example, we could create multiple HttpPlugin for different server frameworks, provide more settings into those plugins, etc. That could allow us to still spin up additional endpoints (like we are doing for the app manifest). I just think it's more transparent to allow moving this logic outside of our plugin model, so that it's more obvious how they can plug into their existing code.

This is particularly relevant for folks transitioning from Teams AI v1, who are used to the server being decoupled from our SDK.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions