Documentation
¶
Overview ¶
Package blwa provides a batteries-included framework for building HTTP services on AWS Lambda Web Adapter (LWA).
Overview ¶
blwa handles the boilerplate of setting up an HTTP server optimized for Lambda: environment parsing, structured logging, OpenTelemetry tracing, AWS SDK clients, and graceful shutdown. A complete application can be created in a single call:
blwa.NewApp[Env](func(m *blwa.Mux, h *Handlers) {
m.HandleFunc("GET /items", h.ListItems)
m.HandleFunc("GET /items/{id}", h.GetItem, "get-item")
},
blwa.WithAWSClient(dynamodb.NewFromConfig),
blwa.WithFx(fx.Provide(NewHandlers)),
).Run()
Environment Configuration ¶
Define your environment by embedding BaseEnvironment:
type Env struct {
blwa.BaseEnvironment
MainTableName string `env:"MAIN_TABLE_NAME,required"`
}
BaseEnvironment provides the following environment variables:
| Variable | Required | Default | Description | |-------------------------------|----------|---------|------------------------------------------------------| | AWS_LWA_PORT | Yes | - | Port the HTTP server listens on | | AWS_LWA_READINESS_CHECK_PATH | Yes | - | Health check endpoint path for LWA readiness | | AWS_REGION | Yes | - | AWS region (set automatically by Lambda runtime) | | BW_SERVICE_NAME | Yes | - | Service name for logging and tracing | | BW_PRIMARY_REGION | Yes | - | Primary deployment region (injected by CDK) | | BW_LAMBDA_TIMEOUT | Yes | - | Lambda function timeout (e.g., "30s", "5m") | | BW_LOG_LEVEL | No | info | Log level (debug, info, warn, error) | | BW_OTEL_EXPORTER | No | stdout | Trace exporter: "stdout" or "xrayudp" | | BW_GATEWAY_ACCESS_LOG_GROUP | No | - | API Gateway access log group for X-Ray correlation | | AWS_LWA_ERROR_STATUS_CODES | Yes | - | HTTP status codes that indicate Lambda errors |
The AWS_LWA_* variables match the official Lambda Web Adapter configuration, so values you set for LWA are automatically picked up by blwa. AWS_REGION is set automatically by the Lambda runtime, while BW_PRIMARY_REGION is injected by the bwcdklwalambda CDK construct.
Runtime ¶
Runtime provides access to app-scoped dependencies and should be injected into handler constructors via fx. This follows idiomatic Go patterns where app-level dependencies are passed explicitly, not pulled from context.
Runtime provides:
- Runtime.Env returns the typed environment configuration
- Runtime.Reverse generates URLs for named routes
- Runtime.Secret retrieves secrets from AWS Secrets Manager
Example handler struct with Runtime:
type Handlers struct {
rt *blwa.Runtime[Env]
dynamo *dynamodb.Client
}
func NewHandlers(rt *blwa.Runtime[Env], dynamo *dynamodb.Client) *Handlers {
return &Handlers{rt: rt, dynamo: dynamo}
}
func (h *Handlers) GetItem(ctx context.Context, w bhttp.ResponseWriter, r *http.Request) error {
env := h.rt.Env() // typed environment
url, _ := h.rt.Reverse("get-item", id) // URL generation
h.dynamo.GetItem(ctx, ...) // direct client access
// ...
}
Secrets ¶
Runtime.Secret retrieves secrets from AWS Secrets Manager with caching. Secrets are fetched per-request to support rotation without redeployment.
// Raw string secret
apiKey, err := h.rt.Secret(ctx, "my-api-key-secret")
// JSON secret with nested path extraction (uses gjson syntax)
// e.g., secret contains: {"database": {"host": "...", "password": "secret123"}}
password, err := h.rt.Secret(ctx, "my-db-credentials", "database.password")
Context ¶
Handlers receive a standard context.Context. Use the package-level functions to access request-scoped values:
func (h *Handlers) GetItem(ctx context.Context, w bhttp.ResponseWriter, r *http.Request) error {
blwa.Log(ctx).Info("fetching item")
blwa.Span(ctx).AddEvent("fetching item")
if lwa := blwa.LWA(ctx); lwa != nil {
// running in Lambda
}
env := h.rt.Env() // from Runtime, not context
// ...
}
Available functions:
- Log - trace-correlated zap logger
- Span - current OpenTelemetry span for custom instrumentation
- LWA - Lambda execution context (request ID, deadline, etc.)
Tracing ¶
OpenTelemetry tracing is configured automatically based on BW_OTEL_EXPORTER:
- "stdout" (default): Pretty-printed spans for local development
- "xrayudp": X-Ray UDP exporter for Lambda with proper trace ID format
The tracer provider and propagator are injected explicitly (no globals), allowing for proper testing and isolation.
When BW_GATEWAY_ACCESS_LOG_GROUP is set (injected automatically by bwcdkrestgateway), the log group is added to trace segments via the aws.log.group.names resource attribute. This enables X-Ray's "View Logs" feature to query API Gateway access logs alongside Lambda function logs.
AWS Clients ¶
AWS SDK v2 clients are registered with WithAWSClient and injected directly into handler constructors via fx. This eliminates reflection and makes dependencies explicit in the type system.
Local Region Clients (Default) ¶
For clients that should use the Lambda's local region (AWS_REGION), register the client factory directly. The client type is injected as-is:
// Registration
blwa.WithAWSClient(func(cfg aws.Config) *dynamodb.Client {
return dynamodb.NewFromConfig(cfg)
})
// Injection - receives *dynamodb.Client directly
func NewHandlers(dynamo *dynamodb.Client) *Handlers {
return &Handlers{dynamo: dynamo}
}
Primary Region Clients ¶
For clients that must target the primary deployment region (BW_PRIMARY_REGION), wrap the client with Primary to make the region explicit in the type:
// Registration
blwa.WithAWSClient(func(cfg aws.Config) *blwa.Primary[ssm.Client] {
return blwa.NewPrimary(ssm.NewFromConfig(cfg))
}, blwa.ForPrimaryRegion())
// Injection - receives *blwa.Primary[ssm.Client]
func NewHandlers(ssm *blwa.Primary[ssm.Client]) *Handlers {
return &Handlers{ssm: ssm}
}
// Usage - access via .Client field
h.ssm.Client.GetParameter(ctx, ...)
Common use cases for primary region clients:
- Generating S3 presigned URLs that work across all regions
- Publishing to centralized SQS queues or SNS topics
- Accessing primary-region-only resources (e.g., certain AWS services)
Fixed Region Clients ¶
For clients that must target a specific region, wrap with InRegion:
// Registration
blwa.WithAWSClient(func(cfg aws.Config) *blwa.InRegion[s3.Client] {
return blwa.NewInRegion(s3.NewFromConfig(cfg), "eu-central-1")
}, blwa.ForRegion("eu-central-1"))
// Injection - receives *blwa.InRegion[s3.Client]
func NewHandlers(s3 *blwa.InRegion[s3.Client]) *Handlers {
return &Handlers{s3: s3}
}
// Usage - access client and region via fields
h.s3.Client.PutObject(ctx, ...)
log.Info("uploading", zap.String("region", h.s3.Region))
Common use cases for fixed region clients:
- Accessing S3 buckets in specific regions
- Targeting SQS queues in particular regions
- Cross-region replication operations
Multiple Region Types Together ¶
A handler can inject clients for different regions simultaneously:
type Handlers struct {
dynamo *dynamodb.Client // local region
ssm *blwa.Primary[ssm.Client] // primary region
s3 *blwa.InRegion[s3.Client] // fixed region
}
func NewHandlers(
dynamo *dynamodb.Client,
ssm *blwa.Primary[ssm.Client],
s3 *blwa.InRegion[s3.Client],
) *Handlers {
return &Handlers{dynamo: dynamo, ssm: ssm, s3: s3}
}
HTTP Client ¶
blwa provides an instrumented HTTP client for outbound requests. All three abstraction levels are available via fx injection:
- http.RoundTripper — instrumented transport for building custom clients
- *http.Client — ready-to-use client with tracing
- Runtime.NewRequest — fluent API via github.com/carlmjohnson/requests
Using Runtime.NewRequest (Recommended) ¶
The simplest way to make outbound requests. Each call returns a fresh requests.Builder with the instrumented transport pre-wired:
func (h *Handlers) FetchData(ctx context.Context, w bhttp.ResponseWriter, _ *http.Request) error {
var result DataResponse
err := h.rt.NewRequest().
BaseURL("https://api.example.com/v1/data").
ToJSON(&result).
Fetch(ctx)
if err != nil {
return err
}
// ...
}
Injecting *http.Client ¶
For handlers that prefer the standard library client:
func NewHandlers(rt *blwa.Runtime[Env], client *http.Client) *Handlers {
return &Handlers{rt: rt, client: client}
}
Injecting http.RoundTripper ¶
For handlers that need a custom client (e.g., with specific timeouts or redirect policy) but still want tracing on the transport:
func NewHandlers(rt *blwa.Runtime[Env], transport http.RoundTripper) *Handlers {
return &Handlers{
rt: rt,
client: &http.Client{
Transport: transport,
Timeout: 5 * time.Second,
},
}
}
All three use the same TracerProvider and Propagator as the inbound server tracing, so outbound requests automatically become child spans of the active trace with propagated context headers.
Timeouts ¶
HTTP server timeouts are configured based on BW_LAMBDA_TIMEOUT to match the Lambda function's execution limit. This differs from traditional internet-facing servers because Lambda Web Adapter acts as a local proxy—the HTTP server is not directly exposed to untrusted clients.
A two-tier timeout strategy is used:
- Server-level timeouts: Based on BW_LAMBDA_TIMEOUT, these act as outer bounds.
- Per-request deadline: Derived from the Lambda invocation deadline (via x-amzn-lambda-context header), this takes precedence when available.
The per-request deadline includes a 500ms buffer for cleanup and error responses. Use RequestDeadline and RequestRemainingTime to check the effective deadline.
See timeout.go for detailed documentation on the timeout strategy and rationale.
Error Status Codes ¶
AWS_LWA_ERROR_STATUS_CODES tells Lambda Web Adapter which HTTP response codes indicate a Lambda function error. This is critical for correct error handling in event-driven architectures:
Without proper error status codes configured:
- SQS triggers: Failed messages are deleted instead of returned to the queue, causing silent data loss. Messages that should be retried are lost forever.
- SNS/EventBridge: Retries don't trigger because Lambda reports success.
- API Gateway: CloudWatch Lambda error metrics are inaccurate.
blwa requires this variable and validates it at startup. By default, the following status codes must be included:
- 500 (Internal Server Error): Catches unhandled exceptions and general errors. Without this, application crashes would be treated as successful responses.
- 504 (Gateway Timeout): Catches timeout errors from WithRequestDeadline. When a request exceeds the Lambda deadline, the handler returns 504 to signal that the function ran out of time. This ensures timeout failures trigger retries.
- 507 (Insufficient Storage): Catches response buffer overflow errors. When a handler generates a response larger than the configured buffer limit, bhttp returns 507. This helps identify handlers that need larger limits or streaming.
The recommended configuration covers all server errors:
AWS_LWA_ERROR_STATUS_CODES=500-599
The format supports comma-separated values and ranges:
- Single codes: "500,502,504"
- Ranges: "500-599"
- Mixed: "500,502-504,599"
To customize which codes are required, use ParseEnvWithRequiredStatusCodes:
blwa.NewApp[Env](routes,
blwa.WithEnvParser(blwa.ParseEnvWithRequiredStatusCodes[Env](500, 502, 503, 504)),
)
Health Checks ¶
A health endpoint is automatically registered at AWS_LWA_READINESS_CHECK_PATH (required env var). Lambda Web Adapter uses this to determine readiness. Customize with WithHealthHandler.
Dependency Injection ¶
blwa uses go.uber.org/fx for dependency injection. Add custom providers with WithFx:
blwa.WithFx(
fx.Provide(NewHandlers),
fx.Provide(NewRepository),
)
Example ¶
Example demonstrates a complete blwa application with local region AWS clients. AWS clients are injected directly into handler constructors via fx.
package main
import (
"context"
"encoding/json"
"net/http"
"github.com/advdv/bhttp"
"github.com/advdv/bhttp/blwa"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"go.uber.org/fx"
"go.uber.org/zap"
)
// Env defines the environment variables for the application.
// Embed blwa.BaseEnvironment to get the required LWA fields.
type Env struct {
blwa.BaseEnvironment
MainTableName string `env:"MAIN_TABLE_NAME,required"`
}
// ItemHandlers contains the HTTP handlers for item operations.
// Dependencies are injected via the constructor, including AWS clients.
type ItemHandlers struct {
rt *blwa.Runtime[Env]
dynamo *dynamodb.Client
}
func NewItemHandlers(rt *blwa.Runtime[Env], dynamo *dynamodb.Client) *ItemHandlers {
return &ItemHandlers{rt: rt, dynamo: dynamo}
}
// ListItems returns all items from the database.
// Demonstrates: blwa.Log() for trace-correlated logging, Runtime.Env for configuration access.
func (h *ItemHandlers) ListItems(ctx context.Context, w bhttp.ResponseWriter, _ *http.Request) error {
env := h.rt.Env()
blwa.Log(ctx).Info("listing items from table",
zap.String("table", env.MainTableName))
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(map[string]any{
"table": env.MainTableName,
"items": []string{"item-1", "item-2"},
})
}
// GetItem returns a single item by ID.
// Demonstrates: blwa.Span() for adding trace events, Runtime.Reverse for URL generation.
func (h *ItemHandlers) GetItem(ctx context.Context, w bhttp.ResponseWriter, r *http.Request) error {
id := r.PathValue("id")
blwa.Span(ctx).AddEvent("fetching item")
selfURL, _ := h.rt.Reverse("get-item", id)
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(map[string]any{
"id": id,
"self": selfURL,
})
}
// CreateItem creates a new item in DynamoDB.
// Demonstrates: Direct AWS client injection, blwa.LWA() for Lambda context.
func (h *ItemHandlers) CreateItem(ctx context.Context, w bhttp.ResponseWriter, _ *http.Request) error {
if lwa := blwa.LWA(ctx); lwa != nil {
blwa.Log(ctx).Info("lambda context",
zap.String("request_id", lwa.RequestID),
zap.Duration("remaining", lwa.RemainingTime()),
)
}
_ = h.dynamo
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
return json.NewEncoder(w).Encode(map[string]string{
"id": "new-item-123",
"status": "created",
})
}
func main() {
blwa.NewApp[Env](
func(m *blwa.Mux, h *ItemHandlers) {
m.HandleFunc("GET /items", h.ListItems)
m.HandleFunc("GET /items/{id}", h.GetItem, "get-item")
m.HandleFunc("POST /items", h.CreateItem)
},
// Local region DynamoDB client - injected directly as *dynamodb.Client
blwa.WithAWSClient(func(cfg aws.Config) *dynamodb.Client {
return dynamodb.NewFromConfig(cfg)
}),
blwa.WithFx(fx.Provide(NewItemHandlers)),
).Run()
}
Example (FixedRegion) ¶
Example_fixedRegion demonstrates fixed region AWS client injection. Use InRegion[T] wrapper when you need to access resources in a specific region (e.g., S3 buckets that must be in a particular region).
package main
import (
"context"
"encoding/json"
"net/http"
"github.com/advdv/bhttp"
"github.com/advdv/bhttp/blwa"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"go.uber.org/fx"
"go.uber.org/zap"
)
// Env defines the environment variables for the application.
// Embed blwa.BaseEnvironment to get the required LWA fields.
type Env struct {
blwa.BaseEnvironment
MainTableName string `env:"MAIN_TABLE_NAME,required"`
}
// UploadHandlers demonstrates fixed region client injection.
type UploadHandlers struct {
rt *blwa.Runtime[Env]
s3 *blwa.InRegion[s3.Client]
}
func NewUploadHandlers(rt *blwa.Runtime[Env], s3 *blwa.InRegion[s3.Client]) *UploadHandlers {
return &UploadHandlers{rt: rt, s3: s3}
}
// Upload uploads a file to a fixed-region S3 bucket.
// Demonstrates: Fixed region client injection using InRegion[T] wrapper.
func (h *UploadHandlers) Upload(ctx context.Context, w bhttp.ResponseWriter, _ *http.Request) error {
blwa.Log(ctx).Info("uploading to fixed region S3",
zap.String("region", h.s3.Region))
_ = h.s3.Client
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(map[string]string{
"status": "uploaded",
"region": h.s3.Region,
})
}
func main() {
blwa.NewApp[Env](
func(m *blwa.Mux, h *UploadHandlers) {
m.HandleFunc("POST /upload", h.Upload)
},
// Fixed region S3 client - wrapped with InRegion[T]
blwa.WithAWSClient(func(cfg aws.Config) *blwa.InRegion[s3.Client] {
return blwa.NewInRegion(s3.NewFromConfig(cfg), "eu-central-1")
}, blwa.ForRegion("eu-central-1")),
blwa.WithFx(fx.Provide(NewUploadHandlers)),
).Run()
}
Example (HttpClient) ¶
Example_httpClient demonstrates using Runtime.NewRequest for outbound HTTP requests. Each call to NewRequest() returns a fresh, instrumented requests.Builder. Outbound requests automatically become child spans of the active trace.
package main
import (
"context"
"encoding/json"
"net/http"
"github.com/advdv/bhttp"
"github.com/advdv/bhttp/blwa"
"go.uber.org/fx"
)
// Env defines the environment variables for the application.
// Embed blwa.BaseEnvironment to get the required LWA fields.
type Env struct {
blwa.BaseEnvironment
MainTableName string `env:"MAIN_TABLE_NAME,required"`
}
// HTTPClientHandlers demonstrates outbound HTTP requests using Runtime.NewRequest.
type HTTPClientHandlers struct {
rt *blwa.Runtime[Env]
}
func NewHTTPClientHandlers(rt *blwa.Runtime[Env]) *HTTPClientHandlers {
return &HTTPClientHandlers{rt: rt}
}
// FetchData demonstrates making an outbound HTTP GET with automatic tracing
// using the fluent requests.Builder API via Runtime.NewRequest.
func (h *HTTPClientHandlers) FetchData(ctx context.Context, w bhttp.ResponseWriter, _ *http.Request) error {
var result map[string]any
err := h.rt.NewRequest().
BaseURL("https://jsonplaceholder.typicode.com").
Pathf("/posts/%d", 1).
ToJSON(&result).
Fetch(ctx)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(result)
}
// PostData demonstrates making an outbound HTTP POST with JSON body
// and automatic tracing via Runtime.NewRequest.
func (h *HTTPClientHandlers) PostData(ctx context.Context, w bhttp.ResponseWriter, _ *http.Request) error {
var result map[string]any
err := h.rt.NewRequest().
BaseURL("https://jsonplaceholder.typicode.com/posts").
BodyJSON(map[string]any{"title": "foo", "body": "bar"}).
ToJSON(&result).
Fetch(ctx)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
return json.NewEncoder(w).Encode(result)
}
func main() {
blwa.NewApp[Env](
func(m *blwa.Mux, h *HTTPClientHandlers) {
m.HandleFunc("GET /fetch", h.FetchData)
m.HandleFunc("POST /post", h.PostData)
},
blwa.WithFx(fx.Provide(NewHTTPClientHandlers)),
).Run()
}
Example (MultiRegion) ¶
Example_multiRegion demonstrates using all three region types together. This is a common pattern where you need: - Local region clients for low-latency data access - Primary region clients for shared configuration - Fixed region clients for specific resources
package main
import (
"context"
"encoding/json"
"net/http"
"github.com/advdv/bhttp"
"github.com/advdv/bhttp/blwa"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/ssm"
"go.uber.org/fx"
"go.uber.org/zap"
)
// Env defines the environment variables for the application.
// Embed blwa.BaseEnvironment to get the required LWA fields.
type Env struct {
blwa.BaseEnvironment
MainTableName string `env:"MAIN_TABLE_NAME,required"`
}
// MultiRegionHandlers demonstrates all three region types in one handler.
type MultiHandlers struct {
rt *blwa.Runtime[Env]
dynamo *dynamodb.Client
ssm *blwa.Primary[ssm.Client]
s3 *blwa.InRegion[s3.Client]
}
func NewMultiHandlers(
rt *blwa.Runtime[Env],
dynamo *dynamodb.Client,
ssm *blwa.Primary[ssm.Client],
s3 *blwa.InRegion[s3.Client],
) *MultiHandlers {
return &MultiHandlers{rt: rt, dynamo: dynamo, ssm: ssm, s3: s3}
}
func (h *MultiHandlers) Process(ctx context.Context, w bhttp.ResponseWriter, _ *http.Request) error {
blwa.Log(ctx).Info("processing with multi-region clients",
zap.String("s3_region", h.s3.Region))
_ = h.dynamo
_ = h.ssm.Client
_ = h.s3.Client
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(map[string]string{
"status": "processed",
"s3_region": h.s3.Region,
})
}
func main() {
blwa.NewApp[Env](
func(m *blwa.Mux, h *MultiHandlers) {
m.HandleFunc("POST /process", h.Process)
},
// Local region DynamoDB - direct injection
blwa.WithAWSClient(func(cfg aws.Config) *dynamodb.Client {
return dynamodb.NewFromConfig(cfg)
}),
// Primary region SSM - wrapped with Primary[T]
blwa.WithAWSClient(func(cfg aws.Config) *blwa.Primary[ssm.Client] {
return blwa.NewPrimary(ssm.NewFromConfig(cfg))
}, blwa.ForPrimaryRegion()),
// Fixed region S3 - wrapped with InRegion[T]
blwa.WithAWSClient(func(cfg aws.Config) *blwa.InRegion[s3.Client] {
return blwa.NewInRegion(s3.NewFromConfig(cfg), "eu-central-1")
}, blwa.ForRegion("eu-central-1")),
blwa.WithFx(fx.Provide(NewMultiHandlers)),
).Run()
}
Example (PrimaryRegion) ¶
Example_primaryRegion demonstrates primary region AWS client injection. Use Primary[T] wrapper when you need to access resources in the primary deployment region (e.g., shared config in SSM Parameter Store).
package main
import (
"context"
"encoding/json"
"net/http"
"github.com/advdv/bhttp"
"github.com/advdv/bhttp/blwa"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ssm"
"go.uber.org/fx"
)
// Env defines the environment variables for the application.
// Embed blwa.BaseEnvironment to get the required LWA fields.
type Env struct {
blwa.BaseEnvironment
MainTableName string `env:"MAIN_TABLE_NAME,required"`
}
// ConfigHandlers demonstrates primary region client injection.
type ConfigHandlers struct {
rt *blwa.Runtime[Env]
ssm *blwa.Primary[ssm.Client]
}
func NewConfigHandlers(rt *blwa.Runtime[Env], ssm *blwa.Primary[ssm.Client]) *ConfigHandlers {
return &ConfigHandlers{rt: rt, ssm: ssm}
}
// GetConfig fetches configuration from the primary region SSM Parameter Store.
// Demonstrates: Primary region client injection using Primary[T] wrapper.
func (h *ConfigHandlers) GetConfig(ctx context.Context, w bhttp.ResponseWriter, _ *http.Request) error {
blwa.Log(ctx).Info("fetching config from primary region SSM")
_ = h.ssm.Client
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(map[string]string{
"config": "value-from-primary-region",
})
}
func main() {
blwa.NewApp[Env](
func(m *blwa.Mux, h *ConfigHandlers) {
m.HandleFunc("GET /config", h.GetConfig)
},
// Primary region SSM client - wrapped with Primary[T]
blwa.WithAWSClient(func(cfg aws.Config) *blwa.Primary[ssm.Client] {
return blwa.NewPrimary(ssm.NewFromConfig(cfg))
}, blwa.ForPrimaryRegion()),
blwa.WithFx(fx.Provide(NewConfigHandlers)),
).Run()
}
Example (Secrets) ¶
Example_secrets demonstrates retrieving secrets from AWS Secrets Manager. Use Runtime.Secret to fetch raw string secrets or extract values from JSON secrets.
package main
import (
"context"
"encoding/json"
"net/http"
"github.com/advdv/bhttp"
"github.com/advdv/bhttp/blwa"
"go.uber.org/fx"
"go.uber.org/zap"
)
// Env defines the environment variables for the application.
// Embed blwa.BaseEnvironment to get the required LWA fields.
type Env struct {
blwa.BaseEnvironment
MainTableName string `env:"MAIN_TABLE_NAME,required"`
}
// SecretHandlers demonstrates secret retrieval from AWS Secrets Manager.
type SecretHandlers struct {
rt *blwa.Runtime[Env]
}
func NewSecretHandlers(rt *blwa.Runtime[Env]) *SecretHandlers {
return &SecretHandlers{rt: rt}
}
// Connect demonstrates retrieving secrets from AWS Secrets Manager.
// Demonstrates: Runtime.Secret for raw string secrets and JSON path extraction.
func (h *SecretHandlers) Connect(ctx context.Context, w bhttp.ResponseWriter, _ *http.Request) error {
apiKey, err := h.rt.Secret(ctx, "my-api-key-secret")
if err != nil {
return err
}
dbPassword, err := h.rt.Secret(ctx, "my-db-credentials", "database.password")
if err != nil {
return err
}
blwa.Log(ctx).Info("retrieved secrets",
zap.Int("api_key_len", len(apiKey)),
zap.Int("password_len", len(dbPassword)))
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(map[string]string{
"status": "connected",
})
}
func main() {
blwa.NewApp[Env](
func(m *blwa.Mux, h *SecretHandlers) {
m.HandleFunc("POST /connect", h.Connect)
},
blwa.WithFx(fx.Provide(NewSecretHandlers)),
).Run()
}
Index ¶
- Constants
- Variables
- func AWSClientProvider[T any](factory func(aws.Config) T, opts ...ClientOption) fx.Option
- func FormatRequiredErrorStatusCodes(codes []int) string
- func FxOptions[E Environment](routing any, opts ...Option) []fx.Option
- func Log(ctx context.Context) *zap.Logger
- func NewAWSConfig(ctx context.Context) (aws.Config, error)
- func NewHTTPClient(t http.RoundTripper) *http.Client
- func NewHTTPTransport(tp trace.TracerProvider, prop propagation.TextMapPropagator) http.RoundTripper
- func NewLogger(env Environment) (*zap.Logger, error)
- func NewPropagator(env Environment) propagation.TextMapPropagator
- func NewServer(params ServerParams, cfg ServerConfig) *http.Server
- func NewTracerProvider(lc fx.Lifecycle, env Environment) (trace.TracerProvider, error)
- func ParseEnv[E Environment]() func() (E, error)
- func ParseEnvWithRequiredStatusCodes[E Environment](requiredCodes ...int) func() (E, error)
- func RequestDeadline(ctx context.Context) (time.Time, bool)
- func RequestRemainingTime(ctx context.Context) time.Duration
- func Span(ctx context.Context) trace.Span
- func TestSetLWAContext(ctx context.Context, lc *LWAContext) context.Context
- func ValidateErrorStatusCodes(errorStatusCodes string, requiredCodes ...int) error
- func WithRequestDeadline(buffer time.Duration) bhttp.Middleware
- type AWSSecretReader
- type App
- type AppConfig
- type BaseEnvironment
- type ClientOption
- type Environment
- type InRegion
- type LWAContext
- type LWAEnvConfig
- type Mux
- type Option
- type Primary
- type Region
- type Runtime
- type RuntimeParams
- type SecretReader
- type ServerConfig
- type ServerParams
- type TimeoutConfig
Examples ¶
Constants ¶
const DefaultDeadlineBuffer = 500 * time.Millisecond
DefaultDeadlineBuffer is the default time reserved before the Lambda deadline for cleanup, error responses, and graceful shutdown.
const LambdaMaxResponsePayloadBytes = 6*1024*1024 - 1024
LambdaMaxResponsePayloadBytes is AWS Lambda's 6 MiB limit minus 1 KiB headroom for JSON/API Gateway overhead.
Variables ¶
var DefaultRequiredErrorStatusCodes = []int{500, 504, 507}
DefaultRequiredErrorStatusCodes are the HTTP status codes that must be present in AWS_LWA_ERROR_STATUS_CODES for correct Lambda error handling.
500 (Internal Server Error): Catches general application errors. Without this, unhandled exceptions would be treated as successful responses.
504 (Gateway Timeout): Catches timeout errors from the WithRequestDeadline middleware. When a request exceeds the Lambda deadline, the handler returns 504 to signal that the function ran out of time. This is critical because timeout errors should trigger retries for SQS/event sources.
507 (Insufficient Storage): Catches response buffer overflow errors. When a handler generates a response larger than the configured buffer limit, bhttp returns 507 to indicate the server cannot store the response representation. This helps identify handlers that need larger buffer limits or response streaming.
Functions ¶
func AWSClientProvider ¶
AWSClientProvider creates an fx.Option that provides an AWS client for injection. The factory receives an aws.Config with the region already configured.
For local region clients (default), the factory returns *T directly:
blwa.WithAWSClient(func(cfg aws.Config) *dynamodb.Client {
return dynamodb.NewFromConfig(cfg)
})
For primary region clients, wrap with Primary[T]:
blwa.WithAWSClient(func(cfg aws.Config) *blwa.Primary[ssm.Client] {
return blwa.NewPrimary(ssm.NewFromConfig(cfg))
}, blwa.ForPrimaryRegion())
For fixed region clients, wrap with InRegion[T]:
blwa.WithAWSClient(func(cfg aws.Config) *blwa.InRegion[sqs.Client] {
return blwa.NewInRegion(sqs.NewFromConfig(cfg), "us-east-1")
}, blwa.ForRegion("us-east-1"))
func FormatRequiredErrorStatusCodes ¶ added in v0.6.3
FormatRequiredErrorStatusCodes formats the required status codes for display.
func FxOptions ¶ added in v0.7.0
func FxOptions[E Environment](routing any, opts ...Option) []fx.Option
FxOptions builds the []fx.Option used by both NewApp and blwatest.New.
func NewAWSConfig ¶
NewAWSConfig loads the default AWS SDK v2 configuration.
func NewHTTPClient ¶ added in v0.7.1
func NewHTTPClient(t http.RoundTripper) *http.Client
NewHTTPClient creates an *http.Client that uses the instrumented transport. Outbound requests automatically create child spans and propagate trace context.
func NewHTTPTransport ¶ added in v0.7.1
func NewHTTPTransport(tp trace.TracerProvider, prop propagation.TextMapPropagator) http.RoundTripper
NewHTTPTransport creates an HTTP RoundTripper instrumented with OpenTelemetry tracing. The TracerProvider and Propagator are explicitly injected to avoid global state. Use this when you need a custom *http.Client but still want outbound request tracing.
func NewLogger ¶
func NewLogger(env Environment) (*zap.Logger, error)
NewLogger creates a zap logger configured from the environment. Uses JSON encoding suitable for CloudWatch. LOG_LEVEL controls the level (debug, info, warn, error).
func NewPropagator ¶
func NewPropagator(env Environment) propagation.TextMapPropagator
NewPropagator creates a TextMapPropagator based on the exporter type. For xrayudp: uses X-Ray propagator for AWS Lambda environments. For stdout/default: uses W3C TraceContext + Baggage composite propagator.
func NewServer ¶
func NewServer(params ServerParams, cfg ServerConfig) *http.Server
NewServer creates an HTTP server with all middleware and routing configured.
func NewTracerProvider ¶
func NewTracerProvider(lc fx.Lifecycle, env Environment) (trace.TracerProvider, error)
NewTracerProvider creates and configures the OpenTelemetry TracerProvider. Supported exporters via OTEL_EXPORTER env var: "stdout" (default), "xrayudp" (Lambda). Shutdown is handled automatically via fx.Lifecycle.
func ParseEnv ¶
func ParseEnv[E Environment]() func() (E, error)
ParseEnv parses environment variables into the given Environment type.
func ParseEnvWithRequiredStatusCodes ¶ added in v0.6.3
func ParseEnvWithRequiredStatusCodes[E Environment](requiredCodes ...int) func() (E, error)
ParseEnvWithRequiredStatusCodes parses environment variables and validates that AWS_LWA_ERROR_STATUS_CODES contains the specified required status codes.
func RequestDeadline ¶ added in v0.6.3
RequestDeadline returns the context deadline for the current request. Returns the zero time and false if no deadline is set.
func RequestRemainingTime ¶ added in v0.6.3
RequestRemainingTime returns the duration until the request context deadline. Returns 0 if no deadline is set or if the deadline has passed.
func TestSetLWAContext ¶ added in v0.6.3
func TestSetLWAContext(ctx context.Context, lc *LWAContext) context.Context
TestSetLWAContext injects an LWAContext into the context for testing purposes. This should only be used in tests to simulate Lambda execution environment.
func ValidateErrorStatusCodes ¶ added in v0.6.3
ValidateErrorStatusCodes parses an AWS_LWA_ERROR_STATUS_CODES string and validates that it contains all required status codes.
The format supports comma-separated values and ranges:
- Single codes: "500,502,504"
- Ranges: "500-599"
- Mixed: "500,502-504,599"
Returns an error if the string cannot be parsed or if any required code is missing.
func WithRequestDeadline ¶ added in v0.6.3
func WithRequestDeadline(buffer time.Duration) bhttp.Middleware
WithRequestDeadline returns middleware that sets a context deadline based on the Lambda invocation deadline from LWAContext.
When the x-amzn-lambda-context header is present (indicating Lambda execution), the context deadline is set to the invocation deadline minus a buffer. This ensures handlers and downstream calls respect the Lambda timeout and have time for graceful cleanup.
If no LWA context is available (e.g., local development), the context is passed through unchanged, and server-level timeouts apply.
Types ¶
type AWSSecretReader ¶
type AWSSecretReader struct {
// contains filtered or unexported fields
}
AWSSecretReader implements SecretReader using AWS Secrets Manager caching client.
func NewAWSSecretReader ¶
func NewAWSSecretReader(cfg aws.Config) (*AWSSecretReader, error)
NewAWSSecretReader creates a new AWSSecretReader using the provided AWS config.
func (*AWSSecretReader) GetSecretString ¶
GetSecretString retrieves a secret value from AWS Secrets Manager with caching.
type App ¶
type App struct {
// contains filtered or unexported fields
}
App wraps an fx.App for lifecycle management.
func NewApp ¶
func NewApp[E Environment](routing any, opts ...Option) *App
NewApp creates a batteries-included app with dependency injection.
The routing function can request any types that are provided via fx options. At minimum, it should accept *Mux for routing.
Example:
blwa.NewApp[Env](func(m *blwa.Mux, h *Handlers) {
m.HandleFunc("GET /items", h.ListItems, "list-items")
},
blwa.WithAWSClient(func(cfg aws.Config) *dynamodb.Client {
return dynamodb.NewFromConfig(cfg)
}),
blwa.WithFx(fx.Provide(NewHandlers)),
).Run()
type AppConfig ¶
type AppConfig struct {
ServerConfig
FxOptions []fx.Option
}
AppConfig holds configuration for the app.
type BaseEnvironment ¶
type BaseEnvironment struct {
Port int `env:"AWS_LWA_PORT,required"`
ServiceName string `env:"BW_SERVICE_NAME,required"`
ReadinessCheckPath string `env:"AWS_LWA_READINESS_CHECK_PATH,required"`
LogLevel zapcore.Level `env:"BW_LOG_LEVEL" envDefault:"info"`
OtelExporter string `env:"BW_OTEL_EXPORTER" envDefault:"stdout"`
AWSRegion string `env:"AWS_REGION,required"`
PrimaryRegion string `env:"BW_PRIMARY_REGION,required"`
// GatewayAccessLogGroup is the CloudWatch Log Group name for API Gateway
// access logs. When set, traces include this log group for X-Ray log
// correlation. Injected automatically by bwcdkrestgateway.
GatewayAccessLogGroup string `env:"BW_GATEWAY_ACCESS_LOG_GROUP"`
// LambdaTimeout is the configured Lambda function timeout. Used to configure
// HTTP server timeouts. Should match the Lambda function's timeout setting.
LambdaTimeout time.Duration `env:"BW_LAMBDA_TIMEOUT,required"`
// ErrorStatusCodes is the raw AWS_LWA_ERROR_STATUS_CODES value. Lambda Web Adapter
// uses this to determine which HTTP status codes indicate Lambda function errors.
// The value supports comma-separated ranges (e.g., "500-599" or "500,502-504").
// This is critical for correct error handling:
// - SQS/event-driven: Without proper error codes, failed messages are deleted
// instead of being retried, causing silent data loss.
// - API Gateway: Enables accurate Lambda error metrics in CloudWatch.
// Validated at startup to ensure it includes 500 (general errors) and 504 (timeouts).
ErrorStatusCodes string `env:"AWS_LWA_ERROR_STATUS_CODES,required"`
}
BaseEnvironment contains the required LWA environment variables. Embed this in your custom environment struct.
type ClientOption ¶
type ClientOption func(*clientOptions)
ClientOption configures AWS client registration.
func ForPrimaryRegion ¶
func ForPrimaryRegion() ClientOption
ForPrimaryRegion configures the client to use the PRIMARY_REGION env var. Use this for cross-region operations that must target the primary deployment region.
The factory should return *blwa.Primary[T] to make the region explicit in the type:
blwa.WithAWSClient(func(cfg aws.Config) *blwa.Primary[ssm.Client] {
return blwa.NewPrimary(ssm.NewFromConfig(cfg))
}, blwa.ForPrimaryRegion())
func ForRegion ¶
func ForRegion(region string) ClientOption
ForRegion configures the client to use a specific fixed region.
The factory should return *blwa.InRegion[T] to make the region explicit in the type:
blwa.WithAWSClient(func(cfg aws.Config) *blwa.InRegion[sqs.Client] {
return blwa.NewInRegion(sqs.NewFromConfig(cfg), "us-east-1")
}, blwa.ForRegion("us-east-1"))
type Environment ¶
type Environment interface {
// contains filtered or unexported methods
}
Environment defines the interface that all environment configurations must implement. Embed BaseEnvironment in your struct to satisfy this interface.
type InRegion ¶
InRegion wraps an AWS client configured for a specific fixed region. Use this when registering and injecting clients that must target a specific region.
Registration:
blwa.WithAWSClient(func(cfg aws.Config) *blwa.InRegion[sqs.Client] {
return blwa.NewInRegion(sqs.NewFromConfig(cfg), "us-east-1")
}, blwa.ForRegion("us-east-1"))
Injection:
func NewHandlers(sqs *blwa.InRegion[sqs.Client]) *Handlers
Usage:
h.sqs.Client.SendMessage(ctx, ...) region := h.sqs.Region // "us-east-1"
func NewInRegion ¶
NewInRegion creates an InRegion wrapper for an AWS client configured for a fixed region. Use this in your client factory when registering with ForRegion():
blwa.WithAWSClient(func(cfg aws.Config) *blwa.InRegion[sqs.Client] {
return blwa.NewInRegion(sqs.NewFromConfig(cfg), "us-east-1")
}, blwa.ForRegion("us-east-1"))
type LWAContext ¶
type LWAContext struct {
RequestID string `json:"request_id"`
Deadline int64 `json:"deadline"`
InvokedFunctionARN string `json:"invoked_function_arn"`
XRayTraceID string `json:"xray_trace_id"`
EnvConfig LWAEnvConfig `json:"env_config"`
}
LWAContext contains Lambda execution context from the x-amzn-lambda-context header.
func LWA ¶
func LWA(ctx context.Context) *LWAContext
LWA retrieves the LWAContext from the request context. Returns nil if not running in a Lambda environment.
func (*LWAContext) DeadlineTime ¶
func (lc *LWAContext) DeadlineTime() time.Time
DeadlineTime returns the Lambda invocation deadline as a time.Time.
func (*LWAContext) RemainingTime ¶
func (lc *LWAContext) RemainingTime() time.Duration
RemainingTime returns the duration until the Lambda invocation deadline.
type LWAEnvConfig ¶
type LWAEnvConfig struct {
FunctionName string `json:"function_name"`
Memory int `json:"memory"`
Version string `json:"version"`
LogGroup string `json:"log_group"`
LogStream string `json:"log_stream"`
}
LWAEnvConfig contains Lambda function environment configuration.
type Option ¶
type Option func(*AppConfig)
Option configures the App.
func WithAWSClient ¶
func WithAWSClient[T any](factory func(aws.Config) T, opts ...ClientOption) Option
WithAWSClient registers an AWS SDK v2 client for dependency injection. Clients are injected directly into handler constructors via fx.
By default, clients target the local region (AWS_REGION env var):
blwa.WithAWSClient(func(cfg aws.Config) *dynamodb.Client {
return dynamodb.NewFromConfig(cfg)
})
For primary region, wrap with Primary[T] and use ForPrimaryRegion():
blwa.WithAWSClient(func(cfg aws.Config) *blwa.Primary[ssm.Client] {
return blwa.NewPrimary(ssm.NewFromConfig(cfg))
}, blwa.ForPrimaryRegion())
For fixed region, wrap with InRegion[T] and use ForRegion():
blwa.WithAWSClient(func(cfg aws.Config) *blwa.InRegion[sqs.Client] {
return blwa.NewInRegion(sqs.NewFromConfig(cfg), "eu-west-1")
}, blwa.ForRegion("eu-west-1"))
func WithHealthHandler ¶
func WithHealthHandler(h func(http.ResponseWriter, *http.Request)) Option
WithHealthHandler sets a custom health check handler. If not set, a default handler returning 200 OK is used.
type Primary ¶
type Primary[T any] struct { Client *T }
Primary wraps an AWS client for the primary deployment region. Use this when registering and injecting clients that must target PRIMARY_REGION.
Registration:
blwa.WithAWSClient(func(cfg aws.Config) *blwa.Primary[ssm.Client] {
return blwa.NewPrimary(ssm.NewFromConfig(cfg))
}, blwa.ForPrimaryRegion())
Injection:
func NewHandlers(ssm *blwa.Primary[ssm.Client]) *Handlers
Usage:
h.ssm.Client.GetParameter(ctx, ...)
func NewPrimary ¶
NewPrimary creates a Primary wrapper for an AWS client configured for the primary region. Use this in your client factory when registering with ForPrimaryRegion():
blwa.WithAWSClient(func(cfg aws.Config) *blwa.Primary[ssm.Client] {
return blwa.NewPrimary(ssm.NewFromConfig(cfg))
}, blwa.ForPrimaryRegion())
type Region ¶
type Region interface {
// contains filtered or unexported methods
}
Region represents a target AWS region for client creation.
func FixedRegion ¶
FixedRegion returns a Region that uses a specific region string.
func LocalRegion ¶
func LocalRegion() Region
LocalRegion returns a Region that uses the Lambda's AWS_REGION.
func PrimaryRegion ¶
func PrimaryRegion() Region
PrimaryRegion returns a Region that uses the PRIMARY_REGION env var. Use this for cross-region operations that must target the primary deployment region.
type Runtime ¶
type Runtime[E Environment] struct { // contains filtered or unexported fields }
Runtime provides access to app-scoped dependencies. Inject this into handler constructors via fx instead of pulling from context.
Example:
type Handlers struct {
rt *blwa.Runtime[Env]
dynamo *dynamodb.Client
}
func NewHandlers(rt *blwa.Runtime[Env], dynamo *dynamodb.Client) *Handlers {
return &Handlers{rt: rt, dynamo: dynamo}
}
func (h *Handlers) GetItem(ctx context.Context, w bhttp.ResponseWriter, r *http.Request) error {
env := h.rt.Env()
url, _ := h.rt.Reverse("get-item", id)
h.dynamo.GetItem(ctx, ...)
// ...
}
func NewRuntime ¶
func NewRuntime[E Environment](env E, mux *Mux, params RuntimeParams) *Runtime[E]
NewRuntime creates a new Runtime with the given dependencies.
func (*Runtime[E]) NewRequest ¶ added in v0.7.1
NewRequest returns a fresh requests.Builder pre-configured with the instrumented HTTP transport. Each call returns a new builder, so there is no risk of shared mutable state between requests.
Example:
var result OrderResponse
err := h.rt.NewRequest().
BaseURL("https://api.example.com/v1/orders").
BodyJSON(&CreateOrderRequest{Amount: 1000}).
ToJSON(&result).
Fetch(ctx)
func (*Runtime[E]) Reverse ¶
Reverse returns the URL for a named route with the given parameters. The route must have been registered with a name using Handle/HandleFunc.
func (*Runtime[E]) Secret ¶
func (r *Runtime[E]) Secret(ctx context.Context, secretID string, jsonPath ...string) (string, error)
Secret retrieves a secret value from AWS Secrets Manager.
The secretID is the secret name or ARN to read from (required). If jsonPath is provided, the secret is parsed as JSON and the path is extracted using gjson syntax (e.g., "database.password", "api.keys.0"). If jsonPath is omitted, the raw secret string is returned.
Secrets are cached but fetched per-request to support rotation without redeployment.
Example:
// Raw string secret apiKey, err := h.rt.Secret(ctx, "my-api-key-secret") // JSON secret with path extraction password, err := h.rt.Secret(ctx, "my-db-credentials", "password")
type RuntimeParams ¶
type RuntimeParams struct {
SecretReader SecretReader
Transport http.RoundTripper
}
RuntimeParams holds optional dependencies for Runtime.
type SecretReader ¶
type SecretReader interface {
GetSecretString(ctx context.Context, secretID string) (string, error)
}
SecretReader abstracts secret retrieval for testability and flexibility.
type ServerConfig ¶
type ServerConfig struct {
HealthHandler func(http.ResponseWriter, *http.Request)
}
ServerConfig holds optional configuration for the HTTP server.
type ServerParams ¶
type ServerParams struct {
fx.In
Env Environment
Mux *Mux
Logger *zap.Logger
TracerProv trace.TracerProvider
Propagator propagation.TextMapPropagator
}
ServerParams holds the dependencies for creating an HTTP server.
type TimeoutConfig ¶ added in v0.6.3
type TimeoutConfig struct {
// LambdaTimeout is the configured Lambda function timeout from infrastructure.
// Used as the basis for server-level timeouts.
LambdaTimeout time.Duration
// DeadlineBuffer is subtracted from the Lambda invocation deadline to allow
// time for cleanup and error responses. Defaults to DefaultDeadlineBuffer.
DeadlineBuffer time.Duration
}
TimeoutConfig holds timeout configuration for the HTTP server.
func (TimeoutConfig) ServerTimeouts ¶ added in v0.6.3
func (tc TimeoutConfig) ServerTimeouts() (readHeaderTimeout, readTimeout, writeTimeout, idleTimeout time.Duration)
ServerTimeouts returns the recommended http.Server timeout values based on the Lambda function timeout. These serve as outer bounds; per-request deadlines from LWAContext take precedence via the WithRequestDeadline middleware.
Server timeouts are set to LambdaTimeout minus DeadlineBuffer. This ensures the server times out before Lambda hard-kills the function, allowing time for graceful error responses.