Helios plugins are self-contained middleware components that intercept and modify HTTP requests and responses flowing through the Helios Gateway. They provide a powerful mechanism to add cross-cutting functionality—such as logging, authentication, rate limiting, and caching—without altering core application logic.
By making use of plugins, you can keep your gateway's architecture clean, modular, and easy to maintain.
The plugin lifecycle is designed for predictability and control.
- Registration: When Helios starts, each plugin's
init()function is called, registering a factory function with the central plugin registry. - Instantiation: The gateway reads the
helios.yamlconfiguration and uses the factory functions to create instances of each configured plugin. - Execution: When a request arrives, it travels through the plugin chain in the exact order defined in the configuration.
The execution order is crucial. The first plugin in your configuration is the outermost layer—it's the first to process a request and the last to process the response. An example request and response flow diagram with two plugins is shown below.
sequenceDiagram
participant Client
participant Helios
participant PluginA as Authentication Plugin
participant PluginB as Logging Plugin
participant Backend
Client->>Helios: Request
Helios->>PluginA: Forward Request
PluginA->>PluginB: Forward Request
PluginB->>Backend: Forward Request
Backend-->>PluginB: Response
PluginB-->>PluginA: Forward Response
PluginA-->>Helios: Forward Response
Helios-->>Client: Response
Plugins are ideal for implementing features that are not specific to a single backend service, often called "cross-cutting concerns." Common use cases include:
- Security: Implement API key validation, JWT verification, or role-based access control at the edge.
- Observability: Add consistent logging, metrics, and tracing to all traffic.
- Traffic Shaping: Enforce rate limiting or block malicious actors by IP address.
- Request/Response Transformation: Add or remove headers, or modify the request path before forwarding.
A Helios plugin is a standard Go HTTP middleware created by a factory function.
- Middleware Type (
plugins.Middleware): A function that takes the nexthttp.Handlerin the chain and returns a newhttp.Handler.type Middleware func(next http.Handler) http.Handler
- Factory Type (
plugins.factory): A function that takes the plugin's name and configuration map and returns aMiddleware.type factory func(name string, cfg map[string]interface{}) (Middleware, error)
All plugins reside in the internal/plugins/ directory. To create a new plugin, add your file to this directory.
internal/
└── plugins/
├── headers.go
├── logging.go
└── registry.go
You may use subdirectories in order to organize complex plugins. For example, internal/plugins/myplugin/.
Plugin configuration is managed in helios.yaml under the plugins key.
enabled(boolean): Enables or disables the plugin system.chain(array): A list of plugin objects to execute in order.name(string): The registered name of the plugin.config(map, optional): Plugin-specific configuration (explained later).
Example helios.yaml:
plugins:
enabled: true
chain:
- name: headers
config:
set:
X-Powered-By: "Helios Gateway"
- name: loggingplugins.RegisterBuiltin(name string, f factory): Registers a new plugin. This function should be called from theinit()function of your plugin file.http.Handler.ServeHTTP(w http.ResponseWriter, r *http.Request): The core method for handling requests. Your plugin will callnext.ServeHTTP(w, r)to pass control to the next middleware.
You can modify the request and response directly within your plugin:
- Request: Modify headers with
r.Header.Set("X-My-Header", "value"). - Response: Modify headers with
w.Header().Set("X-My-Header", "value"). Note that response headers must be set beforenext.ServeHTTPis called if you want them to be available to downstream middleware.
The cfg map[string]interface{} passed to your factory function contains the raw, unmarshaled configuration from helios.yaml. It is your responsibility to parse and validate this map.
If your factory function encounters an invalid configuration, it should return an error. This will prevent Helios from starting and provide clear feedback to the user.
func MyPluginFactory(name string, cfg map[string]interface{}) (Middleware, error) {
apiKey, ok := cfg["apiKey"].(string)
if !ok || apiKey == "" {
return nil, fmt.Errorf("apiKey is required for plugin %s", name)
}
// ...
return middleware, nil
}Let's build a request-id plugin that adds a unique ID to every request and response.
Create a new file: internal/plugins/request_id.go.
package plugins
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
)
func init() {
RegisterBuiltin("request-id", func(name string, cfg map[string]interface{}) (Middleware, error) {
// This plugin has no configuration, so the factory is simple.
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. Generate a unique request ID
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
// Log the error and proceed without a request ID for this request
fmt.Printf("Error generating request ID: %v\n", err)
next.ServeHTTP(w, r)
return
}
requestID := hex.EncodeToString(b)
// 2. Add the ID to the request header
r.Header.Set("X-Request-ID", requestID)
// 3. Add the ID to the response header
w.Header().Set("X-Request-ID", requestID)
// 4. Call the next handler in the chain
next.ServeHTTP(w, r)
})
}, nil
})
}init(): We register our plugin with the namerequest-id.- Factory: Our factory is simple because this plugin doesn't require configuration. It returns the middleware directly.
- Middleware:
- We generate a new UUID for each request.
- We use
r.Header.Setto add the ID to the incoming request. - We use
w.Header().Setto add the same ID to the outgoing response. - Finally, we call
next.ServeHTTPto continue the request chain.
Enable the plugin in your helios.yaml:
plugins:
enabled: true
chain:
- name: request-id
- name: loggingRun Helios and make a request. You should see the X-Request-ID header in both the request received by your backend and the response received by the client.
Plugins can maintain state using Go closures within their factory functions. Variables declared in the factory's scope (outside the returned Middleware or http.HandlerFunc) are "closed over" and persist for the plugin instance's lifetime, accessible by all requests processed by that instance.
Key Considerations:
- Thread Safety: When multiple concurrent requests access shared state, use synchronization primitives like
sync.Mutexto prevent race conditions. - Cleanup: For state that accumulates over time (e.g., request logs, caches), implement periodic cleanup mechanisms (e.g., a background goroutine with
time.Tick) to manage memory usage.
Example: Simple Request Counter
This example demonstrates a basic request counter per remote IP address.
func init() {
RegisterBuiltin("my-plugin", func(name string, cfg map[string]interface{}) (Middleware, error) {
// State stored in closure, protected by a mutex
var mu sync.Mutex
state := make(map[string]int) // Maps IP to request count
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
state[r.RemoteAddr]++ // Increment count for the client's IP
// Optionally, log or use the state for logic (e.g., rate limiting)
mu.Unlock()
next.ServeHTTP(w, r)
})
}, nil
})
}This approach allows plugins to implement complex logic requiring persistent data across requests, such as rate limiting, authentication session tracking, or custom metrics collection.
Although Helios is one of the fastest gateways in the market, you may consider these best practices to improve performance.
- Avoid Blocking Operations: Middleware functions execute synchronously in the request path. Avoid long-running or blocking operations (e.g., complex database queries, external API calls without timeouts) directly within the
http.HandlerFunc. If such operations are necessary, consider offloading them. - Asynchronous Work with Goroutine Pools: For tasks that can be performed asynchronously (e.g., sending logs to a remote service, non-critical metrics collection), use goroutines. To manage resource consumption and prevent unbounded goroutine creation, consider implementing or utilizing a goroutine pool.
- Minimize Allocations: Go's garbage collector can introduce pauses. Reduce memory pressure by minimizing allocations within the hot path of your middleware. This includes:
- Reusing Buffers: For I/O operations, reuse byte buffers where possible.
- Avoiding Unnecessary Copies: Be mindful of data structures that might cause implicit copies.
- Efficient
http.ResponseWriterWrapping: When you need to inspect or modify the HTTP response (e.g., capture status codes or body content), you often need to wrap thehttp.ResponseWriter. Theinternal/plugins/logging.goplugin provides a good example of an efficientstatusRecorderthat wraps thehttp.ResponseWriterto capture the status code without excessive overhead, while also correctly implementinghttp.Hijackerandhttp.Flusherinterfaces for compatibility.
Plugins will eventually have the ability to integrate with Helios's global metrics collector to emit custom metrics, providing deeper insights into plugin-specific behavior and performance. This allows for a unified observability strategy across the gateway and its extensions.
The general pattern for integrating with metrics will involve:
- Importing the Metrics Package:
import "github.com/0xReLogic/Helios/internal/metrics"
- Accessing the Metrics Recorder: Within your middleware, you would obtain a metrics recorder instance.
// In the middleware recorder := metrics.GetRecorder() // if exposed
- Emitting Metrics: Use the recorder to increment counters, record histograms, or set gauges.
recorder.IncrementCounter("plugin_requests") // recorder.ObserveHistogram("plugin_latency_ms", durationMs)
Note: At present, the metrics API is not directly exposed for plugin consumption. These are considered future integration points, and the exact API may evolve. This documentation serves to outline the intended pattern for when this functionality becomes available.
Effective testing is crucial for ensuring the reliability and correctness of your Helios plugins. This section outlines a common pattern for writing unit tests for plugins, leveraging Go's built-in testing package and net/http/httptest for simulating HTTP interactions.
func TestMyPlugin(t *testing.T) {
// 1. Create a mock 'next' HTTP handler
// This handler simulates the behavior of the next component in the middleware chain
// (e.g., another plugin or the backend service).
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// For this example, we simply write a 200 OK status.
// In a real test, you might assert request headers, body, or path
// that your plugin is expected to modify before passing it down.
w.WriteHeader(http.StatusOK)
})
// 2. Instantiate your plugin's middleware
// Call your plugin's factory function with a test name and any required configuration.
// The 'factory' function here is a placeholder for your actual plugin factory.
mw, err := factory("test-plugin", map[string]interface{}{
// "configKey": "configValue", // Add any necessary plugin configuration
})
if err != nil {
t.Fatalf("failed to create plugin middleware: %v", err)
}
// 3. Prepare a test HTTP request and response recorder
// `httptest.NewRequest` creates a synthetic incoming request.
// `httptest.NewRecorder` captures the response written by your plugin.
req := httptest.NewRequest("GET", "/test-path", nil)
// Optionally, add headers or a body to the request if your plugin processes them.
// req.Header.Set("X-Test-Header", "value")
rec := httptest.NewRecorder()
// 4. Execute the plugin middleware
// Your plugin's middleware (mw) is applied to the mock handler.
// The combined handler then serves the test request, writing to the recorder.
mw(handler).ServeHTTP(rec, req)
// 5. Assert the outcomes
// Check the recorded response for expected status codes, headers, or body content.
if rec.Code != http.StatusOK {
t.Errorf("expected status %d, got %d", http.StatusOK, rec.Code)
}
// Example: Assert a header set by your plugin
// if rec.Header().Get("X-Plugin-Header") != "expected-value" {
// t.Errorf("expected X-Plugin-Header 'expected-value', got '%s'", rec.Header().Get("X-Plugin-Header"))
// }
}By following this pattern, you can write robust and isolated tests for your Helios plugins, ensuring they function correctly under various conditions and configurations.
You can try out the example plugins in internal/plugins/ directory. The plugins with example_ prefix are example plugins.
The size_limit plugin protects your gateway from resource exhaustion attacks by limiting the size of request and response bodies. When a limit is exceeded, the plugin returns HTTP 413 (Payload Too Large).
Features:
- Separate configurable limits for request and response bodies
- Returns HTTP 413 (Payload Too Large) when limits are exceeded
- Prevents memory exhaustion from malicious large payloads
- Default limits: 10MB for requests, 50MB for responses
Configuration Example:
plugins:
enabled: true
chain:
- name: size_limit
config:
max_request_body: 10485760 # 10MB in bytes
max_response_body: 52428800 # 50MB in bytesConfiguration Options:
| Option | Type | Default | Description |
|---|---|---|---|
max_request_body |
integer | 10485760 (10MB) | Maximum allowed size for request bodies in bytes |
max_response_body |
integer | 52428800 (50MB) | Maximum allowed size for response bodies in bytes |
Implementation Details:
The plugin uses two mechanisms for size limiting:
-
Request Body Limiting: Uses Go's built-in
http.MaxBytesReaderto limit incoming request body size. When a client sends a request body exceeding the limit, the handler will receive an error when attempting to read the body, allowing it to return a 413 status code. -
Response Body Limiting: Wraps the
http.ResponseWriterwith a custom writer that tracks bytes written. If the response would exceed the configured limit:- If no data has been written yet, the plugin returns HTTP 413 immediately
- If some data has already been sent, the plugin stops writing additional data (HTTP status cannot be changed once headers are sent)
Use Cases:
- API Gateway Protection: Prevent clients from uploading extremely large files
- Memory Protection: Protect backend services from sending unexpectedly large responses
- DoS Prevention: Mitigate denial-of-service attacks using large payloads
- Compliance: Enforce organizational policies on maximum request/response sizes
Example: Protecting a File Upload Endpoint
plugins:
enabled: true
chain:
- name: size_limit
config:
max_request_body: 5242880 # 5MB limit for file uploads
max_response_body: 1048576 # 1MB limit for API responsesTesting the Plugin:
# Test with a request exceeding the limit
dd if=/dev/zero bs=1M count=20 | curl -X POST http://localhost:8080/upload \
-H "Content-Type: application/octet-stream" \
--data-binary @-
# Expected: HTTP 413 Payload Too LargeImportant Notes:
- Limits are enforced per-request, not per-connection
- WebSocket connections are not affected by response body limits after the upgrade
- For very large file uploads, consider using streaming or chunked transfer encoding
- Response limiting cannot change HTTP status codes once headers are sent to the client