Connect-RPC Server
pkg/http/connect runs a Connect-RPC server as a SyncModule. One net/http handler serves Connect, gRPC-Web, and gRPC on a single port — cleartext HTTP/2 (h2c) lets gRPC clients connect without TLS.
Register services with WithService. The registrar runs during Init (when the injector is in context) and receives the shared interceptor chain to pass into the Connect constructor.
lakta.NewRuntime( config.NewModule( config.WithConfigDirs(".", "./config"), config.WithArgs(os.Args[1:]), ), tint.NewModule(), slog.NewModule(), otel.NewModule(), connect.NewModule( connect.WithService(func(ctx context.Context, opts []connectrpc.HandlerOption) (string, http.Handler) { return greetv1connect.NewGreetServiceHandler(NewServer(), opts...) }), ),)Registering services
Section titled “Registering services”WithService is the recommended path: it defers handler construction to Init so you can resolve dependencies with lakta.Invoke[T](ctx), and it injects the built-in interceptor chain into your handler.
For a pre-built handler that already carries its own options, use WithHandler(path, handler). Note the shared interceptor chain is not applied to handlers registered this way — the handler is mounted as-is.
Interceptors
Section titled “Interceptors”The interceptor chain is a fixed sequence of named slots (outer to inner). Every slot wraps unary and streaming handlers:
| Slot | Default |
|---|---|
SlotOtel |
otelconnect tracing and metrics; remote span contexts are trusted |
SlotRequestID |
accepts an inbound X-Request-Id only if it is a UUID or ULID, otherwise generates a ULID; puts it in ctx (RequestID(ctx)), the ctx logger and the response |
SlotAccessLog |
one log line per finished call with procedure, code and duration |
SlotErrorBoundary |
renders, logs and records every error from the slots below (see below) |
SlotRecovery |
turns a handler panic into an error with the panic stack |
SlotDeadline |
applies default_deadline to unary calls without one and caps them at max_deadline |
SlotAuth, SlotAuthz |
empty; filled by auth modules or the application |
SlotValidate |
protovalidate on every request message |
SlotUser |
empty; WithInterceptors(...) appends after it |
Replace or fill a slot with WithSlot(slot, factory). Factories run during Init, so they can resolve DI dependencies. Security interceptors must implement WrapStreamingHandler as well as WrapUnary.
WithRequiredSlots(SlotAuth, SlotAuthz) makes Init fail when a listed slot holds no interceptor, and when a handler is mounted outside the slot chain: any WithHandler entry, or a registrar whose path is not a protobuf service. Use it so a service cannot start without its security interceptors.
Error boundary
Section titled “Error boundary”Every failed call is handled once, at the boundary:
- Wire: the error is mapped through
pkg/errors.FromError. For oops errors, anerrors.Class*tag selects the status,Code()becomesErrorInfo.Reason,In()becomesErrorInfo.Domain, andPublic()is the only text the client sees. oops context, stack and cause never reach the wire. - Log: one line with the full error (oops context and stack included), at
ERRORfor 5xx classes andWARNotherwise. - Trace:
span.RecordErrorwitherror.codeanderror.domain; the span status is set to error for 5xx classes only.
var errNoteNotFound = oops.In("note").Code("NOTE_NOT_FOUND").Tags(errors.ClassNotFound).Public("note not found")Limits
Section titled “Limits”Before connect reads the request body, an HTTP middleware refuses GET to procedures unless allow_get is set, and for unary procedures caps the body at read_max_bytes and applies read_timeout. Streams are capped per message by read_max_bytes only. WithHTTPMiddleware adds your own middleware around the whole handler.
Caller identity (mTLS)
Section titled “Caller identity (mTLS)”With WithTLSProvider (for example spiffe.ServerTLS()), the server requires client certificates and h2c is off. An HTTP middleware runs before the body is read: it takes the caller’s SPIFFE ID from the certificate URI SAN (PeerID(ctx)), refuses callers not in allowed_callers with PERMISSION_DENIED (an empty list refuses everyone), and believes X-Client-IP only from trusted_ip_callers (ClientIP(ctx)); the raw header never reaches handlers. Rejections and failed TLS handshakes emit security_event log lines. With require_peer_auth: true, a production profile refuses to start without mTLS or with an empty allowed_callers.
Mounted services
Section titled “Mounted services”WithMountedHook receives the descriptors of every mounted protobuf service at the end of Init (for example to check that every RPC carries an authorization rule). A hook error fails Init. Paths that are not protobuf services are not passed to hooks; with required slots they fail Init instead.
Outbound client
Section titled “Outbound client”pkg/http/connect/client (connectclient) builds clients for service-to-service calls. Pass Client.HTTPClient(), Client.BaseURL() and Client.Options() to a generated NewXxxClient.
- mTLS:
WithTLSProvider(spiffe.ClientTLS("spiffe://td/ns/x/sa/dbsvc"))accepts only the expected server. - Credentials per call, fail-closed:
AsUser(ctx)forwards the inbound user token,AsService(ctx)uses OAuth2 client credentials (token_url,client_id,client_secret_file),WithToken(ctx, raw)uses an explicit token. A call without a choice fails before dialing. - Propagation: request ID, trace context and the verified client IP.
- Deadline:
min(inbound remaining, max_deadline). - Errors: downstream failures become
UNAVAILABLE(transient codes) orINTERNAL; the downstream code and message never reach your caller.DownstreamCode(err)returns the original code for branching.
Transport security
Section titled “Transport security”By default the server listens in plaintext with h2c enabled (WithH2C), so gRPC clients connect without TLS. Configure file-path based TLS to serve HTTP/2-over-TLS instead — when TLS is set, h2c is ignored.
Multi-instance
Section titled “Multi-instance”connect.NewModule( connect.WithName("internal"), connect.WithService(internalRegistrar),),connect.NewModule( connect.WithName("public"), connect.WithPort(9090), connect.WithService(publicRegistrar),),Configuration Reference
Section titled “Configuration Reference”Config path: modules.http.connect.<name>
hosthost specifies the address for the server to bind to
LAKTA_MODULES__HTTP__CONNECT__<NAME>__HOSTportport represents the port number on which the server listens
LAKTA_MODULES__HTTP__CONNECT__<NAME>__PORTh2ch2C enables cleartext HTTP/2 (h2c) so gRPC clients work without TLS. When
LAKTA_MODULES__HTTP__CONNECT__<NAME>__H2Cread_timeoutreadTimeout bounds reading the entire request incl. body for unary
LAKTA_MODULES__HTTP__CONNECT__<NAME>__READ_TIMEOUTread_max_bytesreadMaxBytes caps each request message (connect ResourceExhausted beyond
LAKTA_MODULES__HTTP__CONNECT__<NAME>__READ_MAX_BYTESmax_header_bytesmaxHeaderBytes caps request header size (http.Server.MaxHeaderBytes)
LAKTA_MODULES__HTTP__CONNECT__<NAME>__MAX_HEADER_BYTESdefault_deadlinedefaultDeadline is applied to unary calls whose client sent none
LAKTA_MODULES__HTTP__CONNECT__<NAME>__DEFAULT_DEADLINEmax_deadlinemaxDeadline caps unary client deadlines. Default 0 (no cap)
LAKTA_MODULES__HTTP__CONNECT__<NAME>__MAX_DEADLINEallow_getallowGet permits Connect GET requests (side-effect-free RPCs). Default
LAKTA_MODULES__HTTP__CONNECT__<NAME>__ALLOW_GETallowed_callersallowedCallers lists the SPIFFE IDs allowed to call this server when peer
LAKTA_MODULES__HTTP__CONNECT__<NAME>__ALLOWED_CALLERStrusted_ip_callerstrustedIPCallers lists the SPIFFE IDs whose X-Client-IP header is believed
LAKTA_MODULES__HTTP__CONNECT__<NAME>__TRUSTED_IP_CALLERSrequire_peer_authrequirePeerAuth makes mTLS and a non-empty AllowedCallers mandatory under
LAKTA_MODULES__HTTP__CONNECT__<NAME>__REQUIRE_PEER_AUTHread_header_timeoutreadHeaderTimeout bounds reading request headers (maps to
LAKTA_MODULES__HTTP__CONNECT__<NAME>__READ_HEADER_TIMEOUTtlsTLS configures file-path based transport security. When unset the server
LAKTA_MODULES__HTTP__CONNECT__<NAME>__TLSCode-only options
Section titled “Code-only options”These options can only be set in Go code via With*() functions, not via config files or environment variables.
| Option | Type | Description |
|---|---|---|
WithTLSProvider(...) | | builds the server TLS config during Init |
WithHandler(...) | map[string]http.Handler | registers a pre-built (path, handler) pair directly; spreads a |
WithService(...) | []connect.ServiceRegistrar | is the recommended registrar: it defers handler construction to Init |
WithInterceptors(...) | []connect.Interceptor | appends extra connect interceptors after every slot |
WithSlot(...) | map[connect.Slot]connect.InterceptorFactory | sets the factory for a slot, replacing the built-in one if any |
WithHTTPMiddleware(...) | [] | wraps the server handler; the first registered runs outermost |
WithMountedHook(...) | []connect.MountedHook | registers a hook that receives the mounted services |
WithRequiredSlots(...) | []connect.Slot | makes Init fail when a listed slot has no interceptor, and |