Skip to main content

Otoroshi plugin system

Otoroshi has an extensible plugin system that lets you customize every aspect of request processing. Plugins are Scala classes that implement one or more plugin traits. They are loaded from the classpath and can be attached to any route.

All plugin traits extend NgPlugin, which itself extends StartableAndStoppable, NgNamedPlugin, and InternalEventListener.

Plugin lifecycle steps

Each plugin declares which steps of the request lifecycle it participates in via the steps method. The proxy engine executes plugins in the following order:

StepTraitDescription
MatchRouteNgRouteMatcherAdditional matching logic after the router selects a route. Can reject a route match based on custom criteria
PreRouteNgPreRoutingRuns before access validation. Used to extract values (custom API keys, tokens, etc.) and store them in the request attributes for downstream plugins
ValidateAccessNgAccessValidatorDecides whether the request is allowed to proceed. Returns NgAllowed or NgDenied(result)
TransformRequestNgRequestTransformerTransforms the outgoing request to the backend (headers, body, URL). Can also short-circuit with a direct response
CallBackendNgBackendCallReplaces or wraps the default backend call. Used for custom backends (static responses, mock servers, protocol bridges)
TransformResponseNgRequestTransformerTransforms the response from the backend before sending it to the client
HandlesTunnelNgTunnelHandlerHandles WebSocket tunnel connections
HandlesRequestNgBackendCallHandles the full request (alternative to CallBackend)
SinkNgRequestSinkCatches requests that did not match any route
RouterNgRouterCustom routing logic (replaces the default router)

Plugin metadata

Every plugin must declare the following metadata through the NgNamedPlugin trait:

MethodTypeDescription
nameStringHuman-readable name displayed in the UI
descriptionOption[String]Short description of what the plugin does
visibilityNgPluginVisibilityNgUserLand (visible in UI) or NgInternal (hidden, used internally)
categoriesSeq[NgPluginCategory]One or more categories for organizing plugins in the UI
stepsSeq[NgStep]Which lifecycle steps this plugin participates in
defaultConfigObjectOption[NgPluginConfig]Default configuration object. Return None if the plugin has no configuration
multiInstanceBooleanWhether the plugin can be added multiple times to the same route (default: true)
noJsFormBooleanIf true, the UI will not generate a form from the config schema (default: false)
configFlowSeq[String]Ordered list of config field names for the UI form layout
configSchemaOption[JsObject]JSON schema describing each config field for the UI form

Plugin categories

Categories help organize plugins in the Otoroshi UI:

AccessControl, Authentication, Classic, Custom, Experimental, Headers, Integrations, Logging, Monitoring, Other, Security, ServiceDiscovery, TrafficControl, Transformations, Tunnel, Wasm, Websocket

Plugin types reference

NgPreRouting

Runs before access validation. Typically used to extract custom credentials or enrich the request context.

trait NgPreRouting extends NgPlugin {
def preRoute(
ctx: NgPreRoutingContext
)(implicit env: Env, ec: ExecutionContext): Future[Either[NgPreRoutingError, Done]]
}

Context (NgPreRoutingContext):

FieldTypeDescription
snowflakeStringUnique request ID
requestRequestHeaderThe incoming Play request
routeNgRouteThe matched route
configJsValuePlugin configuration (merged default + instance)
globalConfigJsValueGlobal Otoroshi configuration
attrsTypedMapShared attributes map (read/write)

Return: Right(Done) to continue, Left(NgPreRoutingError) to short-circuit with an error response.

A synchronous variant is available via preRouteSync. Set isPreRouteAsync = false when using the synchronous variant for better performance.

NgAccessValidator

Validates whether a request is allowed to proceed.

trait NgAccessValidator extends NgPlugin {
def access(
ctx: NgAccessContext
)(implicit env: Env, ec: ExecutionContext): Future[NgAccess]
}

Context (NgAccessContext):

FieldTypeDescription
snowflakeStringUnique request ID
requestRequestHeaderThe incoming Play request
routeNgRouteThe matched route
userOption[PrivateAppsUser]Authenticated user (if any)
apikeyOption[ApiKey]API key (if any)
configJsValuePlugin configuration
globalConfigJsValueGlobal Otoroshi configuration
attrsTypedMapShared attributes map

Return: NgAccess.NgAllowed or NgAccess.NgDenied(result).

The context provides a helper deniedAccess(status, message) to craft a properly formatted error response.

NgRequestTransformer

Transforms requests and responses. This is the most versatile plugin type.

trait NgRequestTransformer extends NgPlugin {
def transformRequest(
ctx: NgTransformerRequestContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, NgPluginHttpRequest]]

def transformResponse(
ctx: NgTransformerResponseContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, NgPluginHttpResponse]]

def transformError(
ctx: NgTransformerErrorContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[NgPluginHttpResponse]

def beforeRequest(ctx: NgBeforeRequestContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Unit]
def afterRequest(ctx: NgAfterRequestContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Unit]
}

Performance flags (override to false when not needed):

FlagDefaultDescription
usesCallbackstrueSet to false if you don't use beforeRequest/afterRequest
transformsRequesttrueSet to false if you only transform responses
transformsResponsetrueSet to false if you only transform requests
transformsErrortrueSet to false if you don't handle errors

Request context (NgTransformerRequestContext):

FieldTypeDescription
rawRequestNgPluginHttpRequestThe original unmodified request
otoroshiRequestNgPluginHttpRequestThe request as modified by previous plugins
snowflakeStringUnique request ID
routeNgRouteThe matched route
apikeyOption[ApiKey]API key (if any)
userOption[PrivateAppsUser]Authenticated user (if any)
requestRequestHeaderThe incoming Play request
configJsValuePlugin configuration
attrsTypedMapShared attributes map

Return: Right(modifiedRequest) to continue, Left(result) to short-circuit and return a response directly to the client.

Synchronous variants are available (transformRequestSync, transformResponseSync). Set isTransformRequestAsync = false or isTransformResponseAsync = false when using them.

NgBackendCall

Replaces or wraps the default HTTP call to the backend. Useful for mock backends, protocol translation, or custom logic.

trait NgBackendCall extends NgPlugin {
def useDelegates: Boolean
def callBackend(
ctx: NgbBackendCallContext,
delegates: () => Future[Either[NgProxyEngineError, BackendCallResponse]]
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[NgProxyEngineError, BackendCallResponse]]
}

When useDelegates is true, the delegates function calls the next backend plugin or the default HTTP backend. When false, the default backend is never called.

Helper methods are available on the trait:

MethodDescription
inMemoryBodyResponse(status, headers, body: ByteString)Create a response from an in-memory body
sourceBodyResponse(status, headers, body: Source[ByteString, _])Create a response from a streaming body
emptyBodyResponse(status, headers)Create a response with no body

NgRouteMatcher

Additional matching logic run after the router selects a route candidate.

trait NgRouteMatcher extends NgPlugin {
def matches(ctx: NgRouteMatcherContext)(implicit env: Env): Boolean
}

Return true if the route should be used, false to reject it and try the next route candidate.

NgRequestSink

Catches requests that did not match any route.

trait NgRequestSink extends NgPlugin {
def matches(ctx: NgRequestSinkContext)(implicit env: Env, ec: ExecutionContext): Boolean
def handle(ctx: NgRequestSinkContext)(implicit env: Env, ec: ExecutionContext): Future[Result]
}

matches determines if this sink should handle the request. If multiple sinks match, the first one wins.

NgTunnelHandler

Handles WebSocket tunnel connections. Extends NgAccessValidator to control access before establishing the tunnel.

trait NgTunnelHandler extends NgPlugin with NgAccessValidator {
def handle(ctx: NgTunnelHandlerContext)(implicit env: Env, ec: ExecutionContext): Flow[Message, Message, _]
}

NgWebsocketPlugin

Intercepts and transforms WebSocket messages on an established connection.

trait NgWebsocketPlugin extends NgPlugin {
def onRequestMessage(ctx: NgWebsocketPluginContext, message: WebsocketMessage)(implicit env: Env, ec: ExecutionContext): Future[Either[NgWebsocketError, WebsocketMessage]]
def onResponseMessage(ctx: NgWebsocketPluginContext, message: WebsocketMessage)(implicit env: Env, ec: ExecutionContext): Future[Either[NgWebsocketError, WebsocketMessage]]
}

Set onRequestFlow and/or onResponseFlow to true to enable interception on each direction.

NgWebsocketBackendPlugin

Provides a custom WebSocket backend (replaces the default WebSocket proxy to the backend target).

trait NgWebsocketBackendPlugin extends NgPlugin {
def callBackend(ctx: NgWebsocketPluginContext)(implicit env: Env, ec: ExecutionContext): Flow[PlayWSMessage, PlayWSMessage, _]
}

NgRouter

Provides entirely custom routing logic. Replaces the default Otoroshi router.

trait NgRouter extends NgPlugin {
def findRoute(ctx: NgRouterContext)(implicit env: Env, ec: ExecutionContext): Option[NgMatchedRoute]
}

HTTP request and response models

Plugins work with NgPluginHttpRequest and NgPluginHttpResponse instead of raw Play objects. These are mutable-friendly case classes that represent the HTTP message flowing through the proxy.

NgPluginHttpRequest

FieldTypeDescription
urlStringFull URL
methodStringHTTP method
headersMap[String, String]Request headers
cookiesSeq[WSCookie]Request cookies
versionStringHTTP version
clientCertificateChain() => Option[Seq[X509Certificate]]Client TLS certificate chain (lazy)
bodySource[ByteString, _]Request body as a streaming source
backendOption[NgTarget]Selected backend target

Useful computed fields: path, host, queryString, queryParams, contentType, contentLength, hasBody.

NgPluginHttpResponse

FieldTypeDescription
statusIntHTTP status code
headersMap[String, String]Response headers
cookiesSeq[WSCookie]Response cookies
bodySource[ByteString, _]Response body as a streaming source

Configuration caching

All plugin contexts extend NgCachedConfigContext, which provides efficient configuration parsing with a 5-second TTL cache:

// Parse config using a Play JSON Reads (cached per route + plugin + index)
val config = ctx.cachedConfig(internalName)(MyConfig.format).getOrElse(MyConfig())

// Parse config using a custom function (cached)
val config = ctx.cachedConfigFn(internalName)(json => MyConfig.parse(json))

// Parse config without caching (re-parsed on every call)
val config = ctx.rawConfig(MyConfig.format)

Always prefer cachedConfig in hot paths for performance.