Skip to content

Plugin Migration Guide

DeepIntShield v1.4.x introduces a new plugin interface for HTTP transport layer interception. This guide helps you migrate existing plugins from the v1.3.x TransportInterceptor pattern to the v1.4.x HTTPTransportPreHook and HTTPTransportPostHook pattern.

The HTTP transport interception mechanism changed from a simple function that receives and returns headers/body to a dual-hook pattern that works with both native .so plugins and WASM plugins.

Aspectv1.3.x (TransportInterceptor)v1.4.x+ (Pre/Post Hooks)
SignatureTransportInterceptor(ctx, url, headers, body)HTTPTransportPreHook(ctx, req) + HTTPTransportPostHook(ctx, req, resp)
Return type(headers, body, error)Pre: (*HTTPResponse, error), Post: error
Request typeSeparate headers map, body mapUnified *HTTPRequest struct
Response accessNot availablePost-hook receives *HTTPResponse
ModificationReturn modified mapsModify req/resp in-place
Short-circuitReturn errorReturn *HTTPResponse
WASM supportNoYes
ContextLimited DeepIntShieldContextFull *DeepIntShieldContext with SetValue/Value

The new dual-hook pattern provides:

  1. WASM plugin support - Serializable types work across WASM boundary
  2. Response interception - Post-hook can modify responses before returning to client
  3. Simpler API - No middleware wrapper, direct function call
  4. Better testability - No fasthttp dependency in plugin tests
  5. Full context access - DeepIntShieldContext available for sharing data between hooks
  6. Custom response short-circuits - Return a full response to short-circuit

Remove the fasthttp import if present:

import (
"fmt"
"github.com/maximhq/deepintshield/core/schemas"
// Remove: "github.com/valyala/fasthttp"
)

Before (v1.3.x):

// TransportInterceptor modifies raw HTTP headers and body
func TransportInterceptor(ctx *schemas.DeepIntShieldContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
// Add custom header
headers["X-Custom-Header"] = "value"
// Modify body
body["custom_field"] = "custom_value"
return headers, body, nil
}

After (v1.4.x+):

// HTTPTransportPreHook intercepts requests BEFORE they enter DeepIntShield core
// Modify req in-place. Return (*HTTPResponse, nil) to short-circuit.
func HTTPTransportPreHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
// Add custom header (in-place modification)
req.Headers["x-custom-header"] = "value"
// Modify body (in-place modification)
var body map[string]any
sonic.Unmarshal(req.Body, &body)
body["custom_field"] = "custom_value"
req.Body, _ = sonic.Marshal(body)
// Store values in context for use in post-hook
ctx.SetValue(schemas.DeepIntShieldContextKey("my-plugin-key"), "my-value")
// Return nil to continue, or return &HTTPResponse{} to short-circuit
return nil, nil
}
// HTTPTransportPostHook intercepts responses AFTER they exit DeepIntShield core
// Modify resp in-place. Called in reverse order of pre-hooks.
func HTTPTransportPostHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
// Add response header
resp.Headers["x-processed-by"] = "my-plugin"
// Read values set in pre-hook
if val := ctx.Value(schemas.DeepIntShieldContextKey("my-plugin-key")); val != nil {
fmt.Println("Context value:", val)
}
// Return nil to continue, or return error to short-circuit
return nil
}

In v1.3.x, you received the body as a map[string]any. In v1.4.x, you work with req.Body bytes:

Before (v1.3.x):

func TransportInterceptor(ctx *schemas.DeepIntShieldContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
// Direct map access
body["model"] = "gpt-4"
return headers, body, nil
}

After (v1.4.x+):

import "github.com/bytedance/sonic"
func HTTPTransportPreHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
// Parse body
var body map[string]any
if err := sonic.Unmarshal(req.Body, &body); err == nil {
// Modify body
body["model"] = "gpt-4"
// Update req.Body in-place
req.Body, _ = sonic.Marshal(body)
}
return nil, nil
}
func HTTPTransportPostHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
// Modify response body if needed
var respBody map[string]any
if err := sonic.Unmarshal(resp.Body, &respBody); err == nil {
respBody["plugin_processed"] = true
resp.Body, _ = sonic.Marshal(respBody)
}
return nil
}

v1.3.x:

headers["authorization"] = "Bearer " + token
return headers, body, nil

v1.4.x+:

// In HTTPTransportPreHook - modify request headers
req.Headers["authorization"] = "Bearer " + token
return nil, nil
// In HTTPTransportPostHook - modify response headers
resp.Headers["x-request-id"] = requestID
return nil

v1.3.x:

apiKey := headers["X-API-Key"]

v1.4.x+:

// Use case-insensitive helper for reading (recommended)
apiKey := req.CaseInsensitiveHeaderLookup("X-API-Key")
// Or direct map access (case-sensitive)
apiKey := req.Headers["x-api-key"]

v1.3.x:

func TransportInterceptor(ctx *schemas.DeepIntShieldContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
if headers["x-skip-processing"] == "true" {
return headers, body, nil
}
// Process...
return headers, body, nil
}

v1.4.x+:

func HTTPTransportPreHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
if req.CaseInsensitiveHeaderLookup("x-skip-processing") == "true" {
return nil, nil // Continue without modification
}
// Process...
return nil, nil
}
func HTTPTransportPostHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
// Post-hook always runs unless pre-hook short-circuited
return nil
}

v1.3.x:

func TransportInterceptor(ctx *schemas.DeepIntShieldContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
if headers["x-api-key"] == "" {
return nil, nil, fmt.Errorf("missing API key")
}
return headers, body, nil
}

v1.4.x+:

func HTTPTransportPreHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
if req.CaseInsensitiveHeaderLookup("x-api-key") == "" {
// Return a custom response to short-circuit
return &schemas.HTTPResponse{
StatusCode: 401,
Headers: map[string]string{"Content-Type": "application/json"},
Body: []byte(`{"error": "missing API key"}`),
}, nil
}
return nil, nil
}
func HTTPTransportPostHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
// Not called if pre-hook short-circuited
return nil
}

v1.3.x:

// url parameter contained the full URL
func TransportInterceptor(ctx *schemas.DeepIntShieldContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
// Limited access to URL
return headers, body, nil
}

v1.4.x+:

func HTTPTransportPreHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
// Full access to request properties
method := req.Method // "GET", "POST", etc.
path := req.Path // "/v1/chat/completions"
query := req.Query // map[string]string of query params
pathParams := req.PathParams // map[string]string of path variables (e.g., {model})
return nil, nil
}
func HTTPTransportPostHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
// Access both request and response
statusCode := resp.StatusCode
responseHeaders := resp.Headers
responseBody := resp.Body
_ = statusCode // Use variables...
_ = responseHeaders
_ = responseBody
return nil
}
  1. Build your updated plugin:

    Terminal window
    go build -buildmode=plugin -o my-plugin.so main.go
  2. Update DeepIntShield to v1.4.x:

    Terminal window
    go get github.com/maximhq/deepintshield/core@v1.4.0
  3. Test with a simple request:

    Terminal window
    curl -X POST http://localhost:8080/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{"model": "openai/gpt-4o-mini", "messages": [{"role": "user", "content": "Hello"}]}'
  4. Verify logs show both hooks being called:

    HTTPTransportPreHook called
    PreLLMHook called
    PostLLMHook called
    HTTPTransportPostHook called

Error: plugin: symbol TransportInterceptor not found

This error occurs if DeepIntShield v1.4.x is looking for the old function. Make sure:

  1. You’ve updated to HTTPTransportPreHook and HTTPTransportPostHook
  2. The function signatures match exactly:
    • func HTTPTransportPreHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error)
    • func HTTPTransportPostHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error
  3. You’ve rebuilt the plugin with the correct core version

Make sure you’re assigning back to req.Body in the pre-hook:

// Wrong - body changes lost
var body map[string]any
sonic.Unmarshal(req.Body, &body)
body["model"] = "gpt-4"
// Missing: req.Body = ...
// Correct - body changes applied
var body map[string]any
sonic.Unmarshal(req.Body, &body)
body["model"] = "gpt-4"
req.Body, _ = sonic.Marshal(body) // Assign back!

Make sure you’re modifying resp in the post-hook:

func HTTPTransportPostHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
// Modify response headers
resp.Headers["x-custom-header"] = "value"
// Modify response body
var body map[string]any
sonic.Unmarshal(resp.Body, &body)
body["extra_field"] = "value"
resp.Body, _ = sonic.Marshal(body)
return nil
}

Make sure you’re modifying req.Headers or resp.Headers directly:

// Set request header in pre-hook
req.Headers["x-custom-header"] = "value"
// Set response header in post-hook
resp.Headers["x-custom-header"] = "value"
// Read headers using case-insensitive helper
value := req.CaseInsensitiveHeaderLookup("X-Custom-Header")

Make sure you’re using the correct context key type:

// In pre-hook - set value
ctx.SetValue(schemas.DeepIntShieldContextKey("my-key"), "my-value")
// In post-hook - read value
if val := ctx.Value(schemas.DeepIntShieldContextKey("my-key")); val != nil {
// Use val
}

DeepIntShield v1.4.x introduces a new hook for intercepting streaming response chunks:

This hook is called for each chunk during streaming responses, allowing plugins to modify or filter chunks before they’re sent to the client.

// HTTPTransportStreamChunkHook intercepts streaming chunks BEFORE they're written to the client.
// Modify chunk data or return nil to skip the chunk entirely.
// Only called for streaming responses when using HTTP transport (deepintshield-http).
func HTTPTransportStreamChunkHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, chunk *schemas.DeepIntShieldStreamChunk) (*schemas.DeepIntShieldStreamChunk, error) {
// chunk is a typed struct containing one of:
// - DeepIntShieldTextCompletionResponse (text completion streaming)
// - DeepIntShieldChatResponse (chat completion streaming)
// - DeepIntShieldResponsesStreamResponse (responses API streaming)
// - DeepIntShieldSpeechStreamResponse (speech synthesis streaming)
// - DeepIntShieldTranscriptionStreamResponse (transcription streaming)
// - DeepIntShieldImageGenerationStreamResponse (image generation streaming)
// - DeepIntShieldError (error during streaming)
// Return chunk unchanged to pass through
return chunk, nil
// Return nil to skip/filter this chunk
// return nil, nil
// Return modified chunk
// modifiedChunk := &schemas.DeepIntShieldStreamChunk{DeepIntShieldChatResponse: ...}
// return modifiedChunk, nil
}

Key differences from HTTPTransportPostHook:

AspectHTTPTransportPostHookHTTPTransportStreamChunkHook
When calledAfter complete responsePer-chunk during streaming
InputFull HTTPResponse*DeepIntShieldStreamChunk (typed struct)
Can modifyFull responseIndividual chunk struct
Can skipN/AReturn nil to skip chunk

If your plugin implements HTTPTransportPostHook and you want to also handle streaming responses, add the new hook:

// Existing hook for non-streaming responses
func HTTPTransportPostHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
// Handle complete responses
return nil
}
// NEW: Add this for streaming responses
func HTTPTransportStreamChunkHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, chunk *schemas.DeepIntShieldStreamChunk) (*schemas.DeepIntShieldStreamChunk, error) {
// Handle streaming chunks (typed struct, not raw bytes)
// Return chunk unchanged if no modification needed
return chunk, nil
}