Skip to main content

Plugin & Middleware Development

Kono can be extended with custom plugins and middlewares compiled as Go shared objects (.so). Both use the same loading mechanism — the difference is when and how they execute.

  • Plugin — invoked at a specific phase in the request lifecycle (before scatter or after aggregation). Works with the gateway's sdk.Context.
  • Middleware — wraps the entire flow handler as a standard http.Handler. Executes for every request regardless of upstream results.

Requirements


  • Go 1.25.4
  • The plugin must be compiled with the exact same Go version as the gateway binary. A mismatch causes a panic at startup.
  • Import github.com/starwalkn/kono/sdk — this is the only dependency required.

Writing a Plugin


A plugin implements the sdk.Plugin interface:

type Plugin interface {
Info() PluginInfo
Init(cfg map[string]interface{}) error
Type() PluginType
Execute(ctx Context) error
}

PluginType determines the execution phase:

ConstantPhaseWhat you can do
sdk.PluginTypeRequestBefore upstream scatterRead and modify request headers, add context values
sdk.PluginTypeResponseAfter aggregationRead and modify response headers and body

Example: request plugin that adds a header


package main

import (
"github.com/starwalkn/kono/sdk"
)

type requestIDPlugin struct{}

func NewPlugin() sdk.Plugin { return &requestIDPlugin{} }

func (p *requestIDPlugin) Info() sdk.PluginInfo {
return sdk.PluginInfo{
Name: "add-header",
Description: "Adds a custom header to every upstream request",
Version: "v1.0.0",
Author: "you",
}
}

func (p *requestIDPlugin) Init(_ map[string]interface{}) error {
return nil
}

func (p *requestIDPlugin) Type() sdk.PluginType {
return sdk.PluginTypeRequest
}

func (p *requestIDPlugin) Execute(ctx sdk.Context) error {
ctx.Request().Header.Set("X-Gateway", "kono")
return nil
}

Example: response plugin that transforms the body


package main

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/starwalkn/kono/sdk"
)

type wrapPlugin struct{}

func NewPlugin() sdk.Plugin { return &wrapPlugin{} }

func (p *wrapPlugin) Info() sdk.PluginInfo {
return sdk.PluginInfo{Name: "wrap", Version: "v1.0.0"}
}

func (p *wrapPlugin) Init(_ map[string]interface{}) error {
return nil
}

func (p *wrapPlugin) Type() sdk.PluginType {
return sdk.PluginTypeResponse
}

func (p *wrapPlugin) Execute(ctx sdk.Context) error {
resp := ctx.Response()
if resp == nil || resp.Body == nil {
return nil
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read body: %w", err)
}

wrapped, err := json.Marshal(map[string]json.RawMessage{
"payload": body,
})
if err != nil {
return fmt.Errorf("marshal: %w", err)
}

resp.Body = io.NopCloser(bytes.NewReader(wrapped))
resp.ContentLength = int64(len(wrapped))
ctx.SetResponse(resp)

return nil
}

sdk.Context


sdk.Context is the per-request context passed to every plugin:

type Context interface {
Request() *http.Request // The incoming HTTP request
Response() *http.Response // The aggregated response (nil in request phase)
SetResponse(*http.Response) // Replace the response (response phase only)
}

In the request phase, Response() returns nil — aggregation has not happened yet. In the response phase, both Request() and Response() are available.

warning

SetResponse must be called after any modification to the response — modifying the *http.Response fields directly without calling SetResponse has no effect on the final output.

Writing a Middleware


A middleware implements the sdk.Middleware interface:

type Middleware interface {
Name() string
Init(cfg map[string]interface{}) error
Handler(next http.Handler) http.Handler
}

Optionally, if the middleware holds background resources (goroutines, connections), implement sdk.Closer:

type Closer interface {
Close() error
}

Kono calls Close() on shutdown for any middleware that implements it.

Example: simple logger middleware


package main

import (
"log"
"net/http"
"time"

"github.com/starwalkn/kono/sdk"
)

type loggerMiddleware struct {
enabled bool
}

func NewMiddleware() sdk.Middleware { return &loggerMiddleware{} }

func (m *loggerMiddleware) Name() string { return "simple-logger" }

func (m *loggerMiddleware) Init(cfg map[string]interface{}) error {
if v, ok := cfg["enabled"].(bool); ok {
m.enabled = v
}

return nil
}

func (m *loggerMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !m.enabled {
next.ServeHTTP(w, r)
return
}

start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}

Building the .so File


Both plugins and middlewares are compiled with -buildmode=plugin:

# Plugin
go build -buildmode=plugin -o myplugin.so ./myplugin

# Middleware
go build -buildmode=plugin -o mymiddleware.so ./mymiddleware

The exported entry point must be named exactly NewPlugin for plugins and NewMiddleware for middlewares. Kono looks up these symbols by name at load time.

info

-buildmode=plugin is only supported on Linux and macOS. Windows is not supported.

Project Structure


A typical custom plugin repository looks like this:

myplugin/
├── go.mod # must use the same Go version as Kono
├── main.go # package main, exports NewPlugin()
└── Makefile

go.mod must declare package main is fine — the binary is never run directly.

The go.mod should reference the same github.com/starwalkn/kono/sdk version as the gateway:

module github.com/yourname/myplugin

go 1.25.4

require github.com/starwalkn/kono/sdk v0.3.0

Registering in Configuration


flows:
- path: /api/users/{id}
method: GET
plugins:
- name: add-header # matches Info().Name
source: file
path: /etc/kono/plugins/
config:
some_key: some_value
middlewares:
- name: simple-logger
source: file
path: /etc/kono/middlewares/
config:
enabled: true
aggregation:
strategy: merge
upstreams:
- ...

The path field points to the directory containing the .so file. Kono resolves the full path as {path}/{name}.so.

Execution Order


Middlewares (outermost first)
└── Request plugins (in config order)
└── Upstream scatter
└── Aggregation
└── Response plugins (in config order)

Middlewares wrap the entire handler including plugins. Within each phase, plugins execute in the order they appear in configuration.

Plugins with the same name are deduplicated — if the same plugin is listed twice in a flow, it is loaded and executed only once.